hvac

package module
v0.0.0-...-9333114 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 19, 2025 License: BSD-2-Clause Imports: 10 Imported by: 0

README

FUMC Garland HVAC controller

Multiple components:

Primary Server (golang, on raspberry pi)

  • Coordinate control of devices and UI
  • provide https REST interface to UI
  • communicate with control-points over MQTT

Phase 1 (done):

  • schedule relay states
  • manually control relays

Phase 2: (done)

  • listen to mqtt for temp sensor data and adjust relays to keep zones in temp range
  • based on room schedule occupancy

Phase 3: (started)

  • ML/AI optimization of the schedule based on occupancy

Phase 4:

  • adjust flow controls (dampers on blowers/ducts, valves on water loops)

MQTT server / http/REST server / logic

Control points (raspberry pi with 8-relay units)

Temp sensor

  • Send data to MQTT (Shelly devices will do this job, others too)
  • Using Shelly H&T

Cold/Hot water flow controls (phase N)

  • Arduino devices connected to existing valves / dampers
  • Listen to MQTT, adjust based on command

Model

Zone

A collection of physical spaces (rooms) which are controlled together, old-school schedule controls zones

Room

A physical space - temperature recorded to room, occupancy schedule to roooms

Loop

A physical loop of pipe that moves either hot or cold water to a zone or zones.

Pump

Moves hot or cold water in a loop

Blower

Connected to loops and ducts, moves air to a zone

Valve

Controls flow on a loop (phase 4)

Damper

Controls flow in a duct (phase 4)

Schedule Entry

A time for relays to be engaged.

Target Temp Range

The target temp for a zone at a given time

Example

(phase 1 - schedule based - complete) Desire: cool 127 on Monday Morning data: Room 127 is in zone D, which requires (pumps 1 + blower X) to cool. command: Run zone D on Monday from 5 AM - 7 AM

(phase 2 - occupancy/temp based - complete) Desire: keep Sch. hall comfortable on Wednesday evening Command: Sch. Hall occupied Wed 5-8 PM Outcome: if temp in zone B is above 78, run (pump 1 & blower Z) until sensor reports temp <= 72 starting at 4 PM

(phase 3 - occupancy/temp based + Machine Learning prediction - in progress) Same as Phase 2, but looking at weather forcast + historical data to determine when to best run the zones to keep them in target range

(phase 4 - not started) close down dampners and valves in areas near the bottom of the target range for their zones, open up those near the top of the range...

assumptions/rules

Blowers can run without the loop pump Loop pumps can only run when the blowers are running (lest the chiller freeze over) Boilers run when water temp is too low, we will not control them Chiller runs hybrid, we must start/stop it, but it has some self-protection logic

Zones will have primary temp ranges If a room is scheduled to be occupied, the zone will be adjusted to that room's selected range (avg. across zone)

research

things to read:

https://niektemme.com/2015/08/09/smart-thermostat/

https://medium.com/devthoughts/linear-regression-with-go-ff1701455bcd

https://www.waveshare.com/wiki/RPi_Relay_Board_(B)

Documentation

Index

Constants

View Source
const (
	MaxPumpRunTime    time.Duration = (14 * time.Hour)
	MinPumpRunTime    time.Duration = (30 * time.Minute)
	MaxBlowerRunTime  time.Duration = (14 * time.Hour)
	MinBlowerRunTime  time.Duration = (30 * time.Minute)
	MaxChillerRunTime time.Duration = (14 * time.Hour)
	MinChillerRunTime time.Duration = (30 * time.Minute)
)

min/max durations for user-scheduled run-times, does not affect temp based

View Source
const (
	BlowersTopic         string = "blowers"
	ChillersTopic        string = "chillers"
	PumpsTopic           string = "pumps"
	RoomsTopic           string = "rooms"
	CurrentStateEndpoint string = "currentstate"
	TargetStateEndpoint  string = "targetstate"
	TempEndpoint         string = "temp"     // Shellies use their own topic, this is for other (potential) sensors
	HumidityEndpoint     string = "humidity" // Shellies use their own topic, this is for other (potential) sensors
)

MQTT topic strings

View Source
const QoS byte = 0

QoS is the MQTT Quality of Service value only 0 has been tested

View Source
const ShellyExternalPowerBattLevel = 101

Variables

This section is empty.

Functions

func GetMQTTChan

func GetMQTTChan() chan MQTTRequest

GetMQTTChan returns the command channel to the MQTT subsystem

func StopAll

func StopAll()

StopAll sends a command to all devices to shut down.

Types

type Blower

type Blower struct {
	CurrentStartTime time.Time
	LastStartTime    time.Time
	LastStopTime     time.Time
	Name             string
	Runtime          time.Duration
	FilterTime       uint64
	ID               BlowerID
	HotLoop          LoopID
	ColdLoop         LoopID
	Zone             ZoneID
	Running          bool
}

A blower is a device that blows air over a coil to cool/heat the air blowers are connected to loops, which are driven by pumps a zone is the region of the building serviced by one or more blowers

type BlowerID

type BlowerID uint8

BlowerID a the unique identifier of each individual blower

func (BlowerID) Get

func (b BlowerID) Get() *Blower

Get returns a pointer to a Blower for a given BlowerId

func (BlowerID) Start

func (b BlowerID) Start(duration time.Duration, source string) error

Start verifies that a blower can be enabled and sends the command to the MQTT subsystem, we do not record the activation of the blower, that takes place when the relay-module confirms that the blower has actually started

func (BlowerID) Stop

func (b BlowerID) Stop(source string)

Stop sends the shutdown command to the MQTT subsystem to be sent to the relay-module if there are any pumps on the connected loops running, and no other blowers on the loop are running, the pump is stopped

type Chiller

type Chiller struct {
	CurrentStartTime time.Time
	LastStartTime    time.Time
	LastStopTime     time.Time
	Name             string
	Loops            []LoopID
	Runtime          time.Duration
	ID               ChillerID
	Running          bool
}

type ChillerID

type ChillerID uint8

func (ChillerID) Get

func (p ChillerID) Get() *Chiller

func (ChillerID) PumpsRunning

func (ch ChillerID) PumpsRunning() bool

func (ChillerID) Start

func (ch ChillerID) Start(duration time.Duration, source string) error

func (ChillerID) Stop

func (ch ChillerID) Stop(source string)

type Command

type Command struct {
	Source      string        // a string describing the reasonx:, manual, schedule, auto
	RunTime     time.Duration // the time to run the device/zone
	TargetState bool
}

Command is what the MQTT server sends to the relay controllers

type Config

type Config struct {
	StateStore        string       // the directory in which to store running state
	SystemMode        SystemModeT  // heating/cooling
	ControlMode       ControlModeT // off/manual/schedule/temp
	MQTT              *MQTTConfig  // config for the mqtt server
	HTTPaddr          string       // the address on which to listen :8080
	HTTPStaticDir     string       // the directory that contains the built webui
	HTTPAuthData      string       // the file that contains the HTTP authentication creds
	DataLogFile       string       // the name of the datalog files
	OpenWeatherMapKey string       // API key from OpenWeatherMap
	OpenWeatherMapID  int          // locaiton ID in OpenWeatherMap
	ChillerLockout    bool         // is the chiller locked-out due to being too cold?
	// BoilerLockout     bool         // is the boiler locked-out due to being to warm? // move to per-zone check
	Blowers          []*Blower
	Chillers         []*Chiller
	Dampers          []*Damper
	Loops            []*Loop
	Pumps            []*Pump
	Rooms            []*Room
	Valves           []*Valve
	Zones            []*Zone
	TimeZoneLocation string
}

Config is the system configuration and run-time data a subset of Config is used for the startup configuration

func GetConfig

func GetConfig() *Config

GetConfig returns the global running config, used by various sub-modules

func LoadConfig

func LoadConfig(filename string) (*Config, error)

LoadConfig is called from main() to set up the running configuration

func (*Config) GetChillerFromLoop

func (c *Config) GetChillerFromLoop(id LoopID) ChillerID

func (*Config) GetOccupancySchedule

func (c *Config) GetOccupancySchedule() (*OccupancySchedule, error)

GetOccupancySchedule returns the live schedule

func (*Config) GetSchedule

func (c *Config) GetSchedule() (*ScheduleList, error)

GetSchedule returns the live schedule

func (*Config) SetControlMode

func (c *Config) SetControlMode(cm ControlModeT) error

SetControlMode is called from LoadConfig at startup and when the mode is changed manually

func (*Config) SetSystemMode

func (c *Config) SetSystemMode(sm SystemModeT) error

SetSystemMode sets the system into the requested mode

func (*Config) WriteToStore

func (c *Config) WriteToStore() error

type ControlMode

type ControlMode struct {
	ControlMode ControlModeT
}

ControlMode is the JSON wireformat for the REST request, dumbly named, probably unnecessary

type ControlModeT

type ControlModeT uint8

ControlModeT is the real ControlMode type, used everywhere except the REST request; just simplify the REST...

const (
	ControlManual   ControlModeT = iota // Manual Mode
	ControlSchedule                     // Schedule Individual devices
	ControlTemp                         // Thermostatic mode
	ControlOff                          // Everything off
)

func (ControlModeT) ToString

func (t ControlModeT) ToString() string

ToString returns a friendly name for the ControlModeT

type Damper

type Damper struct {
	Name  string
	ID    damperID
	State int8
	Min   int8
	Max   int8
}

Damper is a type to be used in the future for controlling how "open" a blower is

type DegF

type DegF float32 // uint8

type DeviceID

type DeviceID interface {
	Start(time.Duration, string) error
	Stop(string)
	// contains filtered or unexported methods
}

DeviceID is a generic interface for all devices (pumps, blowers, etc)

type Loop

type Loop struct {
	Name        string
	ID          LoopID
	RadiantZone ZoneID
}

A loop is a physical pipe that moves heated or cooled water to blowers or radiant devices around the campus. Loops connect blowers and pumps; some zones are radiant only and do not have blowers.

type LoopID

type LoopID uint8

LoopID is the unique identifier for a loop

func (LoopID) Start

func (l LoopID) Start(d time.Duration, msg string) error

type MQTTConfig

type MQTTConfig struct {
	ID         string // something randomish (fumcg-hvac-server)
	Auth       string // filename (/etc/hvac-mqtt-auth.json)
	Root       string // base mqtt path component(s) (fumcg)
	ListenAddr string // (":1883")
}

MQTTConfig is the configuration of the MQTT subsystem

type MQTTRequest

type MQTTRequest struct {
	DeviceID DeviceID
	Command  Command
}

The MQTTRequest is a wrapper type which contains both the command and the ID of the device being controlled

type OccupancyNextRunReport

type OccupancyNextRunReport struct {
	Name    string
	NextRun time.Time
}

func NextRunReport

func NextRunReport() []OccupancyNextRunReport

type OccupancyOneTimeEntry

type OccupancyOneTimeEntry struct {
	Start time.Time
	End   time.Time
	Name  string
	Rooms []RoomID
	ID    OccupancyOneTimeID
}

ScheduleEntry is the definition of a job to be run at specified times

type OccupancyOneTimeID

type OccupancyOneTimeID uint8

types to help enforce code cleanliness

type OccupancyRecurringEntry

type OccupancyRecurringEntry struct {
	Name      string
	StartTime string // "6:30"
	EndTime   string // "15:30"
	Weekdays  []time.Weekday
	Rooms     []RoomID
	ID        OccupancyRecurringID
}

ScheduleEntry is the definition of a job to be run at specified times

type OccupancyRecurringID

type OccupancyRecurringID uint8

type OccupancySchedule

type OccupancySchedule struct {
	Recurring []*OccupancyRecurringEntry
	OneTime   []*OccupancyOneTimeEntry
}

The occupancy scheduler tracks when people are expected to be in a room. If a room is marked as occupied it adjusts the heating/cooling target temp

func (*OccupancySchedule) AddOneTimeEntry

func (s *OccupancySchedule) AddOneTimeEntry(e *OccupancyOneTimeEntry) error

AddOneTimeEntry adds a new entry to the one-time scheduler

func (*OccupancySchedule) AddRecurringEntry

func (s *OccupancySchedule) AddRecurringEntry(e *OccupancyRecurringEntry) error

func (*OccupancySchedule) EditOneTimeEntry

func (s *OccupancySchedule) EditOneTimeEntry(e *OccupancyOneTimeEntry) error

EditEntry updates an entry in the OccupancySchedule, keyed based on e.ID

func (*OccupancySchedule) EditRecurringEntry

func (s *OccupancySchedule) EditRecurringEntry(e *OccupancyRecurringEntry) error

EditEntry updates an entry in the OccupancySchedule, keyed based on e.ID

func (*OccupancySchedule) GetOneTimeEntry

GetOneTimeEntry returns an entry based on ID

func (*OccupancySchedule) GetRecurringEntry

func (*OccupancySchedule) RemoveOneTimeEntry

func (s *OccupancySchedule) RemoveOneTimeEntry(id OccupancyOneTimeID)

RemoveOneTimeEntry remove an entry from the running scheduler and the configuration

func (*OccupancySchedule) RemoveRecurringEntry

func (s *OccupancySchedule) RemoveRecurringEntry(id OccupancyRecurringID)

type Pump

type Pump struct {
	CurrentStartTime time.Time
	LastStartTime    time.Time
	LastStopTime     time.Time
	Name             string
	Runtime          time.Duration
	ID               PumpID
	Loop             LoopID
	SystemMode       SystemModeT
	Running          bool
}

type PumpID

type PumpID uint8

func (PumpID) Get

func (p PumpID) Get() *Pump

func (PumpID) Start

func (p PumpID) Start(duration time.Duration, source string) error

Start sends the command to the MQTT subsystem to tell the relay-module to start the pump

func (PumpID) Stop

func (p PumpID) Stop(source string)

Stop sends the command to the MQTT subsystem to tell the relay-module to stop the pump it stops any chillers on the attached loop if no other pumps are runnning

type Relay

type Relay struct {
	StartTime time.Time     // time of current start
	StopTime  time.Time     // time to stop running
	RunTime   time.Duration // total run-time of the device
	Pin       uint8         // gpio pin on relay control board
	PumpID    PumpID        // non-zero if devices is a pump
	BlowerID  BlowerID      // non-zero if device is a blower
	ChillerID ChillerID     // non-zero if the device is a chiller
	Running   bool          // is the device currently running
}

Relay is the primary config type for the relay-module process, set ONE of PumpID, BlowerID or ChillerID the PIN is the GPIO pin which is determined by the device hardware

type Response

type Response struct {
	CurrentState  bool
	RanTime       time.Duration // time actually ran (when task completed)
	TimeRemaining time.Duration // time left on the clock (for periodic check-ins)
}

Response is what the Relay Controller sends to the MQTT server

type Room

type Room struct {
	LastUpdate  time.Time
	Name        string
	ShellyID    string
	Temperature DegF
	ID          RoomID
	CoolZone    ZoneID
	HeatZone    ZoneID
	Humidity    uint8
	Battery     uint8
	Occupied    bool
}

Room is the basic data type for a physical space temperature, humidity, and occupancy are tracked per-room device control is per-zone. This will allow us to track how long different rooms take to bring to proper temperature so we can set start-times properly. When we start building controls for the damnpers and valves having per-room data will help with system tuning even more

func (Room) GetZoneIDInMode

func (r Room) GetZoneIDInMode() ZoneID

func (Room) GetZoneInMode

func (r Room) GetZoneInMode() *Zone

func (*Room) SetBattery

func (r *Room) SetBattery(battery uint8)

SetBattery records the battery status as reported by the sensors, called from MQTT subsystem

func (*Room) SetHumidity

func (r *Room) SetHumidity(humidity uint8)

SetHumidity records the humidity as reported by the sensors, called from MQTT subsystem

func (*Room) SetTemp

func (r *Room) SetTemp(temp DegF)

SetTemp records the temperature as reported by the sensors, called from the MQTT subsystem if the zone average is out-of-range, the proper devices are enabled to bring the zone into temperature

type RoomID

type RoomID uint16

RoomID is the room number or other identifying number uint16 since we've got 3 floors and a uint8 would not be enough

func GetRoomIDFromShelly

func GetRoomIDFromShelly(shellyID string) RoomID

GetRoomIDFromShelly returns a RoomID based on an associated (case insensitive) shelly ID

func (RoomID) Get

func (r RoomID) Get() *Room

Get returns a full room struct for a RoomID

func (RoomID) ToogleOccupancy

func (r RoomID) ToogleOccupancy()

type ScheduleEntry

type ScheduleEntry struct {
	Name      string
	StartTime string // "10:30" "18:00;22:30;24:00""
	Weekdays  []time.Weekday
	Zones     []ZoneID
	RunTime   time.Duration
	ID        uint8
	Mode      SystemModeT
}

ScheduleEntry is the definition of a job to be run at specified times

type ScheduleList

type ScheduleList struct {
	List []*ScheduleEntry
}

ScheduleList is just a wrapper so we can hang methods on it The scheduler is used in "schedule" control mode (not temp) and starts and stops devices based on the schedule, not room temp/occupancy this is akin to the old mode of operation using the scheduler from the 1980s...

func (*ScheduleList) AddEntry

func (s *ScheduleList) AddEntry(e *ScheduleEntry) error

AddEntry adds a new entry to the list of jobs to run

func (*ScheduleList) EditEntry

func (s *ScheduleList) EditEntry(e *ScheduleEntry) error

EditEntry updates an entry in the ScheduleList, keyed based on e.ID

func (*ScheduleList) GetEntry

func (s *ScheduleList) GetEntry(id uint8) *ScheduleEntry

GetEntry returns an entry in the ScheduleList by ID

func (*ScheduleList) RemoveEntry

func (s *ScheduleList) RemoveEntry(id uint8)

RemoveEntry removes a ScheduleEntry from the list by ID

type SystemMode

type SystemMode struct {
	Mode SystemModeT
}

SystemMode is a wrapper type for clean JSON files passed from the REST interface

type SystemModeT

type SystemModeT uint8

SystemModeT is a convenience type to ensure clean code

const (
	SystemModeHeat    SystemModeT = iota // the system is heating
	SystemModeCool                       // the system is cooling
	SystemModeUnknown                    // unused
)

func (SystemModeT) ToString

func (t SystemModeT) ToString() string

ToString returns a friendly string for a SystemModeT

type Valve

type Valve struct {
	Name  string
	ID    ValveID
	State int8
	Min   int8
	Max   int8
}

Valve is a type for future use when we build controllers to adjust the flow on the loops

type ValveID

type ValveID uint8

type Zone

type Zone struct {
	Name             string
	OneDegreeAdjTime time.Duration // how long does it take the zone to move by 1 degF
	Targets          ZoneTargets
	AverageTemp      DegF
	ID               ZoneID
}

A zone is a collection of rooms which are controlled together, either by radiant heat or blowers

func (*Zone) SetTargets

func (z *Zone) SetTargets(c *Config, zt *ZoneTargets) error

SetTargets sets a zone's target tempratures, called from the REST interface

func (*Zone) UpdateTemp

func (z *Zone) UpdateTemp()

type ZoneID

type ZoneID uint8

ZoneID is a unique identifier for a zone

func (ZoneID) Get

func (z ZoneID) Get() *Zone

Get returns a populated zone for a given ZoneID

func (ZoneID) IsRunning

func (z ZoneID) IsRunning() bool

A zone is runing of all devices in the zone are running, no matter how they were started XXX TODO this is not complete for radiant heating zones

func (ZoneID) Start

func (z ZoneID) Start(d time.Duration, msg string) error

Start starts up all devices in a zone if the zone is running, it extends the time the zone is running if the new duration is longer than the current duration e.g. 55 minutes left, called for 60 minutes, extends to 60 minutes. e.g. 120 minutes left, called for 60 minutes, does nothing

func (ZoneID) Stop

func (z ZoneID) Stop(msg string)

Stop shuts down all devices in a zone

type ZoneTargets

type ZoneTargets struct {
	HeatingOccupiedTemp   DegF // 68 -- heat to 68 if someone is schedule to be in the zone
	HeatingUnoccupiedTemp DegF // 60 -- let it get down to 60 if no one is schedule to be in the zone
	CoolingOccupiedTemp   DegF // 74 -- cool to 74 if someone is sheduled to be in the zone
	CoolingUnoccupiedTemp DegF // 80 -- let it get up to 80 if no one is scheduled to be in the zone
}

Each zone has four target temps, based on systemMode and room occupancy

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL