This is somewhat of a work in progress. I’m sharing it here as I’m at a stopping point and I’m not yet sure where I want to go from here.
Goal
Many of us, myself included, have a number of sensors scattered throughout the house yet there is only one thermostat that controls the heat (forced air HVAC in my case). How to best use these various sensors?
I still don’t have the answer but I have the first part solved at least, how to get the readings into HestiaPi.
Prerequisites
I’m running the latest ONE branch of the code from GitHub. The code is significantly different from version 1.1. But over all I’m trying to provide all of the changes external to the stock openHAB configs so they are easier to retain over an upgrade/.
Requirements
- Support more than one sensor
- Require minimal configuration from users
- Require minimal changes to existing openHAB configs
- Have timeouts so if a sensor doesn’t report after too long of a time that sensor is ignored; if no external sensors are reporting default to the built in sensors
MQTT
To minimize the configuration necessary on the HestiaPi side of things I’ve defined a standardized MQTT topic structure:
hestia/external/<room>/temperature
hestia/external/<room>/humidity
The messages are expected to be just the values of the sensor readings without units. The code assumes that the units of the values posted match TempUnit (i.e. degrees F if TempUnit is set to F). For humidity, values between 0 and 100 are assumed.
It should be pretty self explanatory. <room>
can be any arbitrary name supported by MQTT. Configure your sensors to publish to these topics on whatever MQTT broker HestiaPi subscribes to. If your sensors do not support publishing to these topics, you may need to bridge between where they do publish and these topics.
Personally, I have sensors of various technologies, not all MQTT, reporting to my main openHAB. Some of these sensors use Units of Measurement so I can’t forward the readings using the follow Profile. So I wrote a Rule to forward my various sensors to the above topics.
from core.rules import rule
from core.triggers import when
@rule("Forward temps to HestiaPi",
description="Takes current readings and forwards them to the proper MQTT topic")
@when("Member of gIndoorTemps received update")
@when("Member of gIndoorHumidity received update")
def forward(event):
broker = "mqtt:broker:spare"
room = event.itemName.split("_")[0].replace("v", "")
reading = event.itemState.toString() # may need to deal with UoM here
units = "temperature" if "Temp" in event.itemName else "humidity"
topic = "hestia/external/{}/{}".format(room, units)
forward.log.debug("Forwarding {} to {}".format(reading, topic))
action = actions.get("mqtt", broker)
if action:
action.publishMQTT(topic, reading)
else:
forward.log.error("There is no broker thing {}!".format(broker))
NOTE: The above is using the NGRE and is written in Python using the Helper Libraries. My temp Items have “Temp” in the name which is how I separate and publish to the right topics based on the sensor type.
If you don’t have the senors feeding to another OH you have other options. You can change the following code and configs to use whatever topic structure they already follow. You can add Rules to HestiaPi to do this. You might be able to set up the broker as a bridge to itself and forward the original topics to the above topics. I’ve never tried this last one though, may not be possible.
HestiaPi Thing Configuration
Create a new .things file, I’ll call it /etc/openhab2/things/personal.things
. We will create a new Broker Thing to subscribe to the HestiaPi broker and create a couple of publishTrigger
Channels which use a wild card subscription to the above MQTT structure.
Bridge mqtt:broker:mosquitto "External Sensors MQTT Broker" [ host="localhost", secure=false, port=1883, clientID="external"]{
Channels:
Type publishTrigger : externaltemp "External Temperature" [stateTopic="hestia/external/+/temperature", separator="#"]
Type publishTrigger : externalhumi "External Humidity" [stateTopic="hestia/external/+/humidity", separator="#"]
}
A publishTrigger
will trigger a Rule every time a message is received. The separator will be placed between the topic and the message in the receivedEvent
implicit variable in the Rule which we can parse to figure out where the message came from.
You can add the Channels to the existing Bridge in the HestiaPi configuration, but make sure to put the Channels after the definition of all the Things.
HestiaPi Items Configuration
This one is pretty easy. Since we have multiple sensors, what we really care about is the minimum sensor reading and the maximum sensor readings. So we need four new Items, which I place in /etc/openhab2/items/personal.items
.
Number ExternalTempMax "External Max Temperature [%.0f]"
Number ExternalTempMin "External Min Temperature [%.0f]"
Number ExternalHumiMax "External Max Humidity [%.0f %%]"
Number ExternalHumiMin "External Min Humidity [%.0f %%]"
HestiaPi Rules
The Rule gets triggered by the internal sensors as well as the MQTT Trigger Channels defined above. We parse out the source of the sensor reading and the sensor reading and keep them in a Map. For each new reading from MQTT we set a Timer that will eliminate the reading if there isn’t a new one after the right amount of time (10 minutes in the code below).
The tricky part is the end of the Rule. We go through the Map of sensor readings and find the minimum and maximum reading. If we have no external readings at all, we use the built in MyTempProxy/MyHumiProxy Item instead.
Finally, we publish the min and max readings to the new Items.
import java.util.Map
val Map<String, Number> readings = newHashMap
val Map<String, Timer> timers = newHashMap
// Keep track of external sensors
rule "External Temp"
when
Channel "mqtt:broker:mosquitto:externaltemp" triggered or
Channel "mqtt:broker:mosquitto:externalhumi" triggered or
Item MyTempProxy changed or
Item MyHumiProxy changed
then
val logName = "external"
var Number reading = null
var String sensor = null
if(receivedEvent !== null) {
// receivedEvent is of the format: <channel ID> triggered <topic>#<message>
// <topic> is of the format: hestai/external/<room>/<temperature|humidity>
val payload = receivedEvent.toString.split(" ").get(2).split("#")
val topic = payload.get(0)
reading = Float::parseFloat(payload.get(1))
sensor = topic.split("/").get(3)
logDebug(logName, "Reading = {} Topic = {} Sensor = {}", reading, topic, sensor)
readings.put(topic, reading)
// Erase the value if it get's more than 10 minutes old
if(timers.get(topic) === null) {
timers.put(topic, createTimer(now.plusMinutes(10), [ |
logWarn(logName, "{} has not reported in 10 minutes! Deleting reading.", topic)
timers.put(topic, null)
readings.put(topic, null)
]))
}
else {
timers.get(topic).reschedule(now.plusMinutes(10))
}
}
else {
reading = triggeringItem.state as Number
sensor = if(triggeringItem.name.contains("Temp")) "temperature" else "humidity"
}
// Get the min and max for that type of reading, if there are no external readings
// default to the built in sensor
var local = if(sensor == "temperature") MyTempProxy else MyHumiProxy
var min = readings.entrySet.filter[ entry | entry.key.contains(sensor) ].map[ getValue ].reduce[ min, v | if(min > v) v else min ]
if(min === null) min = local.state as Number
var max = readings.entrySet.filter[ entry | entry.key.contains(sensor) ].map[ getValue ].reduce[ max, v | if(max < v) v else max ]
if(max == null) max = local.state as Number
logDebug("external", "{}: Min = {} Max = {}", sensor, min, max)
// Publish the min and max values to the appropriate Items
switch(sensor){
case "temperature":{
ExternalTempMin.postUpdate(min)
ExternalTempMax.postUpdate(max)
}
case "humidity":{
ExternalHumiMin.postUpdate(min)
ExternalHumiMax.postUpdate(max)
}
}
end
Next Steps
The next steps are where I’ve stopped. Now that I have the min and max readings from around the house, what do I do with them? For heating, do I try to make the max temp equal to the SetPoint or the min temp equal to the setpoint? Or do I aim for the middle and try to get the mid point between the min temp and max temp be the setpoint?
There are lots of ways to handle this. Anyway, once you figure out what you want to do with these max and min values, some changes need to be made to the existing HestiaPi Rules.
-
Update/change the PreviousTempReading/PreviousHumiReading Item to kick off the Rule that tries to figure out what to do with the heating/cooling/humidifier/dehumidifier in the Rule above.
-
Remove the updates to the Previous Reading Items from “Process Sensor Changes” Rule so the internal sensor doesn’t drive the thermostat.
-
Modify the heating/cooling/humidity lambdas to decide whether or not to turn on/off the device based on the External Min and Max Items based on the behavior you desire.
Future Ideas
I can make this much more dynamic and easier to customize using the NGRE. For example, I can dynamically create Items for the external sensors and move the min/max logic to a Group definition. I can use the Expire binding on these dynamically created Items to timeout the value. All of this will make the sitemap able to keep up with the external sensor readings, showing all the information available without needing to statically define it.
Also, with NGRE, Rules can call Rules, enable/disable Rules, and support library functions. Thus one could just load these new Rules, run a configuration Rule and it will cut out the old default Rules and insert this new Rule, freeing the user from needing to modify any of the code.