Files
2020-07-09 21:11:52 +02:00

281 lines
12 KiB
Python

from builtins import staticmethod
import logging
from mqtt import MqttMessage, MqttConfigMessage
from workers.base import BaseWorker
import logger
from datetime import datetime, timedelta
import time
REQUIREMENTS = ['python-eq3bt']
monitoredAttrs = ["low_battery", "valve_state", "target_temperature", "window_open", "locked"]
_LOGGER = logger.get(__name__)
STATE_AWAY = 'away'
STATE_BOOST = 'boost'
STATE_AUTO = 'auto'
STATE_MANUAL = 'manual'
STATE_ON = 'on'
STATE_OFF = 'off'
class Eq3BtWorker(BaseWorker):
class ModesMapper():
def __init__(self):
from eq3bt import Mode
self._mapped_modes = {
Mode.Closed: STATE_OFF,
Mode.Open: STATE_ON,
Mode.Auto: STATE_AUTO,
Mode.Manual: STATE_MANUAL,
Mode.Away: STATE_AWAY,
Mode.Boost: STATE_BOOST,
}
self._reverse_modes = {v: k for k, v in self._mapped_modes.items()}
def get_mapping(self, mode):
if mode < 0:
return None
return self._mapped_modes[mode]
def get_reverse_mapping(self, mode):
return self._reverse_modes[mode]
def format_topic(self, *args, separator='/'):
return separator.join([*args])
def mqttHomieDevice(self,device, name, nodes):
return [
MqttConfigMessage(self.format_topic(device), "$state", payload="ready"),
MqttConfigMessage(self.format_topic(device), "$homie", payload="3.0"),
MqttConfigMessage(self.format_topic(device), "$name", payload=name),
MqttConfigMessage(self.format_topic(device), "$nodes", payload=nodes)
]
def mqttHomieNode(self, device, label, name, properties):
return [
MqttConfigMessage(self.format_topic(device, label), "$name", payload=name),
MqttConfigMessage(self.format_topic(device, label), "$properties", payload=properties),
]
def mqttHomieProperty(self, device, node, prop, name, dtype, settable = False, retained = False, unit = None, dformat = None):
retval = [
MqttConfigMessage(self.format_topic(device, node, prop), "$name", payload=name),
MqttConfigMessage(self.format_topic(device, node, prop), "$datatype", payload=dtype),
MqttConfigMessage(self.format_topic(device, node, prop), "$settable", payload="true" if settable else "false"),
MqttConfigMessage(self.format_topic(device, node, prop), "$retained", payload="true" if retained else "false")
]
if unit is not None:
retval.append(MqttConfigMessage(self.format_topic(device, node, prop), "$unit", payload=unit))
if dformat is not None:
retval.append(MqttConfigMessage(self.format_topic(device, node, prop), "$format", payload=dformat))
return retval
def _setup(self):
from eq3bt import Thermostat
_LOGGER.info("Adding %d %s devices", len(self.devices), repr(self))
for name, mac in self.devices.items():
_LOGGER.debug("Adding %s device '%s' (%s)", repr(self), name, mac)
self.devices[name] = {"mac": mac, "thermostat": Thermostat(mac)}
self._modes_mapper = self.ModesMapper()
self._command_queue = []
def config(self):
ret = []
for name, data in self.devices.items():
ret += self.config_device(name, data["mac"])
return ret
def config_device(self, name, mac):
from eq3bt import eq3btsmart
_LOGGER.debug("Configuring device " + str(name))
retval = []
retval = self.mqttHomieDevice(name, "Smart Thermostat: " + str(name) + " (" + str(mac)+ ")", "status,climate")
retval += self.mqttHomieNode(name, "status", "Device Status", "low_battery,window_open,locked,last_updated")
retval += self.mqttHomieProperty(name, "status", "low_battery", "Low Battery", "boolean", False, True)
retval += self.mqttHomieProperty(name, "status", "window_open", "Window Open", "boolean", False, True)
retval += self.mqttHomieProperty(name, "status", "locked", "Lock", "boolean", True, True)
retval += self.mqttHomieProperty(name, "status", "last_updated", "Last Updated", "string", False, True)
retval += self.mqttHomieNode(name, "climate", "Climate Control", "mode,target_temperature,comfort_temperature,eco_temperature,temperature_offset,valve_state")
retval += self.mqttHomieProperty(name, "climate", "mode", "Climate Mode", "enum", True, True, dformat = "auto,manual,boost,away,on,off")
retval += self.mqttHomieProperty(name, "climate", "target_temperature", "Target Temperature", "float", True, True, dformat = str(eq3btsmart.EQ3BT_MIN_TEMP) + ":" + str(eq3btsmart.EQ3BT_MAX_TEMP))
retval += self.mqttHomieProperty(name, "climate", "comfort_temperature", "Comfort Temperature", "float", True, True, dformat = str(eq3btsmart.EQ3BT_MIN_TEMP) + ":" + str(eq3btsmart.EQ3BT_MAX_TEMP))
retval += self.mqttHomieProperty(name, "climate", "eco_temperature", "ECO Temperature", "float", True, True, dformat = str(eq3btsmart.EQ3BT_MIN_TEMP) + ":" + str(eq3btsmart.EQ3BT_MAX_TEMP))
retval += self.mqttHomieProperty(name, "climate", "temperature_offset", "Temperature Offset", "float", True, True)
retval += self.mqttHomieProperty(name, "climate", "valve_state", "Valve State", "integer", False, True)
return retval
def run(self, mqtt):
_LOGGER.info("Starting eq3bt daemon")
from bluepy import btle
self.time_next_update = datetime.now()
commandTryCounter = 0
updateTryCounter = 0
while True:
try:
time_now = datetime.now()
if time_now > self.time_next_update:
_LOGGER.info("Updating %d %s devices", len(self.devices), repr(self))
updateTryCounter += 1
if updateTryCounter <= 3:
for name, data in self.devices.items():
_LOGGER.debug("Updating %s device '%s' (%s)", repr(self), name, data["mac"])
ret = self.update_device_state(name, data["thermostat"])
mqtt.publish(ret)
_LOGGER.debug("Updating %s device '%s' finished", repr(self), name)
updateTryCounter -= 1
else:
_LOGGER.error("Retried update too many times, dropping for now...")
updateTryCounter = 0
self.time_next_update = time_now + timedelta(seconds=self.update_interval)
if len(self._command_queue) > 0:
_LOGGER.info("Executing command")
commandTryCounter += 1
if commandTryCounter <= 5:
command = self._command_queue[0]
if command():
self.time_next_update = time_now
del self._command_queue[0]
commandTryCounter -= 1
_LOGGER.debug("Executing command finished")
else:
_LOGGER.error("Retried command too many times, dropping command...")
del self._command_queue[0]
commandTryCounter = 0
time.sleep(1.0)
except btle.BTLEException as e:
import traceback
_LOGGER.error("Caught BTLEException: {}".format(e))
_LOGGER.error(traceback.format_exc())
import subprocess
_LOGGER.error("Reseting hci0")
try:
subprocess.run(["/bin/hciconfig", "hci0", "down"], timeout=10)
time.sleep(1.0)
subprocess.run(["/bin/hciconfig", "hci0", "up"], timeout=10)
except Exception as e:
_LOGGER.error("Caught exception while reseting hci0: {}".format(e))
_LOGGER.error(traceback.format_exc())
time.sleep(10.0)
except Exception as e:
import traceback
_LOGGER.error("Caught exception in Eq3BtWorker.run(): {}".format(e))
_LOGGER.error(traceback.format_exc())
time.sleep(1.0)
'''
def status_update(self):
from bluepy import btle
ret = []
_LOGGER.info("Updating %d %s devices", len(self.devices), repr(self))
for name, data in self.devices.items():
_LOGGER.debug("Updating %s device '%s' (%s)", repr(self), name, data["mac"])
try:
ret += self.update_device_state(name, data["thermostat"])
except btle.BTLEException as e:
logger.log_exception(_LOGGER, "Error during update of %s device '%s' (%s): %s", repr(self), name, data["mac"], type(e).__name__, suppress=True)
_LOGGER.info("Updating %d %s devices finshed", len(self.devices), repr(self))
return ret
'''
def on_command(self, topic, value):
retval = []
try:
value = value.decode('utf-8')
device_name, nodename, propname, operation = topic.split('/')
_LOGGER.debug("Command \"%s\", on %s.%s.%s: %s", operation, device_name, nodename, propname, value)
device = self.devices[device_name]["thermostat"]
def _cmd_fun():
if propname == "locked" and operation == "set":
device.locked = True
elif value == "false" or value == "off":
device.locked = False
elif propname == "mode" and operation == "set":
device.mode = self._modes_mapper.get_reverse_mapping(value)
elif propname == "target_temperature" and operation == "set":
device.target_temperature = float(value)
elif propname == "comfort_temperature" and operation == "set":
device.comfort_temperature = float(value)
elif propname == "eco_temperature" and operation == "set":
device.eco_temperature = float(value)
elif propname == "temperature_offset" and operation == "set":
device.temperature_offset = float(value)
elif propname == "valve_state" and operation == "set":
device.valve_state = float(value)
return True
self._command_queue.append(_cmd_fun)
except Exception as e:
import traceback
_LOGGER.error("Caught exception in Eq3BtWorker.on_command(): {}".format(e))
_LOGGER.error(traceback.format_exc())
return retval
def update_device_state(self, name, thermostat):
from datetime import datetime
thermostat.update()
ret = []
ret.append(MqttMessage(topic=self.format_topic(name, 'status', 'low_battery'), payload="true" if thermostat.low_battery else "false"))
ret.append(MqttMessage(topic=self.format_topic(name, 'status', 'window_open'), payload="true" if thermostat.window_open else "false"))
ret.append(MqttMessage(topic=self.format_topic(name, 'status', 'locked'), payload="true" if thermostat.locked else "false"))
ha_mode = self._modes_mapper.get_mapping(thermostat.mode)
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'mode'), payload=ha_mode))
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'target_temperature'), payload=thermostat.target_temperature))
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'comfort_temperature'), payload=thermostat.comfort_temperature))
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'eco_temperature'), payload=thermostat.eco_temperature))
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'temperature_offset'), payload=thermostat.temperature_offset))
ret.append(MqttMessage(topic=self.format_topic(name, 'climate', 'valve_state'), payload=thermostat.valve_state))
ret.append(MqttMessage(topic=self.format_topic(name, 'status', 'last_updated'), payload=datetime.isoformat(datetime.now())))
return ret