-
Notifications
You must be signed in to change notification settings - Fork 36
Zone Controllers
Note: this feature is currently only supported on the unitoftime branch
When designing a zone controller, we want to receive up-to-date data on a number of streams that might influence our control decision. Similar to how the scheduler works, we subscribe to sets of points by describing them with metadata queries.
The syntax follows subscribe/<key value> = <metadata where clause>. The zone controller service automatically subscribes to the streams described by the metadata clause, and exposes the latest values in a table internal to the service, self.points. The values are accessible by the <key value> specified in the subscribe declaration, e.g. subscribe/sensor_temp_value = Metadata/System = "Monitoring" and Properties/UnitofMeasure = 'F' will cache the latest sensor value under self.points['sensor_temp_value'], where it can be used programmatically in control decisions.
The configuration profile requires that you include the archiver URL (defaults to 'localhost:8079') as well as the synchronous nature of the controller. If synchronous = True is in the configuration file, then the zone controller will publish its decisions at regular intervals (determined by the rate key in the configuration file). If synchronous = False, then the zone controller will publish a decision only when it receives new data from any of its subscriptions.
As with the scheduler, we include metadata describing the URI endpoints of the zone controller. By including this metadata, we allow sMAP drivers to subscribe to the zone controller in order to have the control decisions realized in a deployment.
[/]
uuid = 9982a2c2-3d10-11e4-8af6-6003089ed1d0
Metadata/Site = 0273d18f-1c03-11e4-a490-6003089ed1d0
[/zonecontroller1]
type = zonecontroller.ZoneController
subscribe/temp_sensor = Metadata/System = 'Monitoring' and Metadata/HVACZone = '410 Soda W' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F'
subscribe/thermostat_temp = Metadata/System = 'HVAC' and Metadata/HVACZone = '410 Soda W' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F'
subscribe/temp_heat = Metadata/System = 'HVAC' and Metadata/Description = 'Master Heating setpoint'
subscribe/temp_cool = Metadata/System = 'HVAC' and Metadata/Description = 'Master Cooling setpoint'
archiver = http://localhost:8079
synchronous = True
[/zonecontroller1/temp_heat]
Metadata/Description = Zone Heating setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda W
[/zonecontroller1/temp_cool]
Metadata/Description = Zone Cooling setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda WThe default zone controller is very simple, and takes nearly all logic directly from the .ini file.
# filename: passthrough.py
from smap.services.zonecontroller import ZoneController
class PassThrough(ZoneController):
def setup(self, opts):
ZoneController.setup(self, opts)
# for each 'subscribe/<key value>' in the .ini file that we want to publish,
# we make sure we add a timeseries endpoint (note: 'F' are the units)
self.add_timeseries('/temp_heat','F',data_type='double')
self.add_timeseries('/temp_cool','F',data_type='double')We can use the same .ini file as above, just changing zonecontroller.ZoneController to passthrough.PassThrough.
This zone controller will take values from its subscriptions (described in the .ini file) and re-publish them at its own endpoints. That is, the streams described by subscribe/temp_heat will be pushed to /temp_heat, assuming that a self.add_timeseries('/temp_heat'...) is included in the setup() method.
As mentioned above, publishing new values can happen at regular intervals (synchronous = True and rate=X in the .ini file), or whenever new readings are received from the subscriptions (synchronous = False).
If we want to do something more interesting with our zone controller, we can add logic to our zone controller inside the step() method. Remember that the most recent values of all the keys in the subscribe/<key value> are in a dictionary self.points. As a simple example, we can add a trim of 5 degrees to our published heating and cooling setpoints:
# filename: 5degreetrim.py
from smap.services.zonecontroller import ZoneController
class TrimController(ZoneController):
def setup(self, opts):
ZoneController.setup(self, opts)
self.add_timeseries('/temp_heat','F',data_type='double')
self.add_timeseries('/temp_cool','F',data_type='double')
def step(self):
# self.points['temp_heat'] is the most recent heating setpoint from the master scheduler
# we add 5 degrees to the most recent value.
self.add('/temp_heat', self.points['temp_heat']+5)
self.add('/temp_cool', self.points['temp_cool']+5)Because step() is called either at a regular interval or triggered by input from a subscription (depending on the value of synchronous in the .ini file), we can use it to execute a control loop. We can execute the next iteration of a PID loop, adjust setpoints, etc.
Most of the time, the default behavior of using the most recent data value from a subscription is good enough. In some cases, however, it can be useful to have the option to do something more advanced.
In the setup() method of our zone controller, we can use the add_callback method to add a new callback on a key. The add_callback method takes 3 arguments: the key, the function to call, and the subscription query to use as a data source. The function must be declared as a method of the zone controller class and take 4 arguments: the mandatory 'self' argument, the key name, 'uuids' and 'data'. 'uuids' contains a list of UUIDs for the timeseries returned by the subscription, and 'data' is a list with each item being an array of data points. Every time a value is received by the subscription, the function is called, allowing us to compute values and place them in self.points for use in the step() method:
from zonecontroller import ZoneController
class AvgTempController(ZoneController):
def setup(self, opts):
ZoneController.setup(self, opts)
self.add_timeseries('/temp_heat','F',data_type='double')
self.add_timeseries('/temp_cool','F',data_type='double')
self.add_callback('temp_sensor', self.avg_temp, opts.get('subscribe/temp_sensor'))
def step(self):
# adjust heat/cool setpoints by the difference between the thermostat temperature and the
# average room setpoint
new_diff = self.points['thermostat_temp'] - self.points['avg_temp']
self.points['temp_heat'] += new_diff
self.points['temp_cool'] += new_diff
self.add('/temp_heat', self.points['temp_heat'])
self.add('/temp_cool', self.points['temp_cool'])
def avg_temp(self, point, uuids, data):
self.points['avg_temp'] = sum(map(lambda x: x[-1][1], data)) / float(len(data))Here, we want to use the average temperature readings of the sensors in a particular HVAC zone, so we use a single subscription to receive all temperature readings. We then use a custom callback to compute the average temperature and place it in the self.points. The step() method makes the simple calculation based on the thermostat temperature (also retrieved from a data subscription) and then publishes the computed values to the relevant endpoints so that they can be retrieved by subscribed sMAP sources.
While it is certainly simpler to statically designate zone controllers and subscriptions in an .ini file before starting a sMAP deployment, if a collection of zone controllers is known beforehand, it is possible to dynamically change the zone controller during a live deployment.
Zone controller configurations must be placed in an .ini file that will not be run by another sMAP process. To guarantee that sMAP doesn't accidentally run the file and produce conflicting zone controllers, it's a good idea to just have the driver configurations themselves without the usual sMAP config headers (e.g. the [/] and [server] sections). We place the following in zc.ini:
# filename "zc.ini"
[/passthrough]
type = followcontroller.FollowMaster
subscribe/temp_sensor = Metadata/System = 'Monitoring' and Metadata/HVACZone = '410 Soda W' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F'
subscribe/temp_heat = Metadata/System = 'HVAC' and Metadata/Description = 'Master Heating setpoint'
subscribe/temp_cool = Metadata/System = 'HVAC' and Metadata/Description = 'Master Cooling setpoint'
archiver = http://localhost:8079
synchronous = True
[/passthrough/temp_heat]
Metadata/Description = Zone Heating setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda W
[/passthrough/temp_cool]
Metadata/Description = Zone Cooling setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda W
[/followtrim]
type = followcontroller.FollowMasterTrim
subscribe/temp_sensor = Metadata/System = 'Monitoring' and Metadata/HVACZone = '410 Soda W' and Metadata/Type = 'Sensor' and Properties/UnitofMeasure = 'F'
subscribe/temp_heat = Metadata/System = 'HVAC' and Metadata/Description = 'Master Heating setpoint'
subscribe/temp_cool = Metadata/System = 'HVAC' and Metadata/Description = 'Master Cooling setpoint'
archiver = http://localhost:8079
synchronous = True
[/followtrim/temp_heat]
Metadata/Description = Zone Heating setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda W
[/followtrim/temp_cool]
Metadata/Description = Zone Cooling setpoint
Metadata/System = HVAC
Metadata/Type = Setpoint
Metadata/HVACZone = 410 Soda WInside our main .ini file, or wherever we would normally place the zone controller, we use the following:
[/zonecontroller]
type = smap.services.zonecontroller.Decider
Metadata/HVACZone = 410 Soda W
section = followzone
configfile = zc.ini- the
Decidercontroller allows us to change which zone controller runs. -
sectionis a reference to which configuration (e.g. which driver) we run by default -
configfileis the path to the configuration file we defined above
To change which zone controller, we send a PUT request to the URL defined by the Decider. In this case, it is /zonecontroller/decider_act. In the body of the PUT request, we put the name of the section we want to switch to. Using the Python Requests library, this would look like
# to use passthrough zone controller
requests.put('http://localhost:8080/zonecontroller/decider_act?state=1',data='passthrough')
# to use followtrim zone controller
requests.put('http://localhost:8080/zonecontroller/decider_act?state=1',data='followtrim')