diff --git a/etrv2mqtt/autodiscovery.py b/etrv2mqtt/autodiscovery.py index ac483ed..53b715c 100644 --- a/etrv2mqtt/autodiscovery.py +++ b/etrv2mqtt/autodiscovery.py @@ -25,7 +25,10 @@ class Autodiscovery(): "min_temp":"10", "max_temp":"40", "temp_step":"0.5", - "modes":["heat"], + "suggested_display_precision": "1", + "modes":["auto"], + "mode_state_topic":"etrv2mqtt/state", + "mode_state_template":"{{ 'auto' }}", "device": { "identifiers":"0000", "manufacturer": "Danfoss", @@ -39,12 +42,15 @@ class Autodiscovery(): _battery_template = json.loads(""" { + "entity_category": "diagnostic", "device_class": "battery", "name": "kitchen battery", "unique_id":"0000_battery", "state_topic": "etrv/kitchen/state", "value_template": "{{ value_json.battery }}", "unit_of_measurement": "%", + "suggested_display_precision": "0", + "state_class": "measurement", "device": { "identifiers":"0000", "manufacturer": "Danfoss", @@ -58,6 +64,7 @@ class Autodiscovery(): _reported_name_template = json.loads(""" { + "entity_category": "diagnostic", "name": "kitchen reported name", "unique_id":"0000_rep_name", "state_topic": "etrv/kitchen/state", @@ -81,6 +88,8 @@ class Autodiscovery(): "state_topic": "etrv/kitchen/state", "value_template": "{{ value_json.room_temp }}", "unit_of_measurement": "°C", + "suggested_display_precision": "1", + "state_class": "measurement", "device": { "identifiers":"0000", "manufacturer": "Danfoss", @@ -94,6 +103,7 @@ class Autodiscovery(): _last_update_template = json.loads(""" { + "entity_category": "diagnostic", "device_class": "timestamp", "name": "Kitchen Last Update", "unique_id":"0000_last_update", @@ -139,6 +149,7 @@ def register_termostat(self, dev_name: str, dev_mac: str) -> AutodiscoveryResult autodiscovery_msg = self._autodiscovery_payload( self._termostat_template, dev_mac, dev_name, "Thermostat") autodiscovery_msg['~'] = self._config.mqtt.base_topic+'/'+dev_name + autodiscovery_msg['mode_state_topic'] = self._config.mqtt.base_topic + "/state" return AutodiscoveryResult(autodiscovery_topic, payload=json.dumps(autodiscovery_msg)) diff --git a/etrv2mqtt/config.py b/etrv2mqtt/config.py index b6af599..eea8f5c 100644 --- a/etrv2mqtt/config.py +++ b/etrv2mqtt/config.py @@ -23,6 +23,7 @@ class _MQTTConfig: autodiscovery: bool autodiscovery_topic: str autodiscovery_retain: bool + device_data_retain: bool hass_birth_topic: str hass_birth_payload: str @@ -70,11 +71,16 @@ def __init__(self, filename: str): _config_json['mqtt']['autodiscovery'], _config_json['mqtt']['autodiscovery_topic'], _config_json['mqtt']['autodiscovery_retain'], + _config_json['mqtt']['device_data_retain'], _config_json['mqtt']['hass_birth_topic'], _config_json['mqtt']['hass_birth_payload'], ) self.retry_limit: int = _config_json['options']['retry_limit'] + self.retry_rerun: bool = _config_json['options']['retry_rerun'] + self.idle_block_ble: bool = _config_json['options']['idle_block_ble'] + self.poll_schedule: str = _config_json['options']['poll_schedule'] self.poll_interval: int = _config_json['options']['poll_interval'] + self.poll_hour_minute: int = _config_json['options']['poll_hour_minute'] self.stay_connected: bool = _config_json['options']['stay_connected'] self.report_room_temperature: bool = _config_json['options']['report_room_temperature'] self.setpoint_debounce_time: int = _config_json['options']['setpoint_debounce_time'] diff --git a/etrv2mqtt/devices.py b/etrv2mqtt/devices.py index 6db6b1e..fb21b46 100644 --- a/etrv2mqtt/devices.py +++ b/etrv2mqtt/devices.py @@ -8,12 +8,14 @@ from etrv2mqtt.etrvutils import eTRVUtils from etrv2mqtt.mqtt import Mqtt from typing import Type, Dict, NoReturn +import os import schedule class DeviceBase(ABC): def __init__(self, thermostat_config: ThermostatConfig, config: Config): super().__init__() + self._config = config @abstractmethod def poll(self, mqtt: Mqtt): @@ -23,7 +25,6 @@ def poll(self, mqtt: Mqtt): def set_temperature(self, mqtt: Mqtt, temperature: float): pass - class TRVDevice(DeviceBase): def __init__(self, thermostat_config: ThermostatConfig, config: Config): super().__init__(thermostat_config, config) @@ -47,8 +48,16 @@ def poll(self, mqtt: Mqtt): if self._stay_connected == False: self._device.disconnect() + return True + except btle.BTLEInternalError as e: + logger.error(e) + if self._device.is_connected(): + self._device.disconnect() except btle.BTLEDisconnectError as e: logger.error(e) + if self._device.is_connected(): + self._device.disconnect() + return False def set_temperature(self, mqtt: Mqtt, temperature: float): try: @@ -57,10 +66,18 @@ def set_temperature(self, mqtt: Mqtt, temperature: float): if not self._device.is_connected(): self._device.connect() eTRVUtils.set_temperature(self._device, temperature) + if self._stay_connected == False: + self._device.disconnect() # Home assistant needs to see updated temperature value to confirm change self.poll(mqtt) except btle.BTLEDisconnectError as e: logger.error(e) + if self._device.is_connected(): + self._device.disconnect() + except btle.BTLEInternalError as e: + logger.error(e) + if self._device.is_connected(): + self._device.disconnect() class DeviceManager(): @@ -78,12 +95,26 @@ def __init__(self, config: Config, deviceClass: Type[DeviceBase]): self._mqtt.hass_birth_callback = self._hass_birth_callback def _poll_devices(self): + self.ble_on() + rerun = [] for device in self._devices.values(): - device.poll(self._mqtt) + if not device.poll(self._mqtt): + if self._config.retry_rerun: + rerun.append(device) + if len(rerun)>0: + logger.warning("Attempting re-run of failed sensors in 5 seconds") + time.sleep(5) + for device in rerun: + device.poll(self._mqtt) + self.ble_off() def poll_forever(self) -> NoReturn: - schedule.every(self._config.poll_interval).seconds.do( - self._poll_devices) + if self._config.poll_schedule == "hour_minute": + schedule.every().hour.at(":{0:02d}".format(self._config.poll_hour_minute)).do( + self._poll_devices) + else: # for poll_schedule=interval and as fallback + schedule.every(self._config.poll_interval).seconds.do( + self._poll_devices) mqtt_was_connected: bool = False while True: @@ -100,7 +131,9 @@ def poll_forever(self) -> NoReturn: time.sleep(2) def _set_temerature_task(self, device: DeviceBase, temperature: float): + self.ble_on() device.set_temperature(self._mqtt, temperature) + self.ble_off() # this will cause the task to be executed only once return schedule.CancelJob @@ -120,3 +153,13 @@ def _set_temperature_callback(self, mqtt: Mqtt, name: str, temperature: float): def _hass_birth_callback(self, mqtt: Mqtt): schedule.run_all(delay_seconds=1) + + def ble_off(self): + if self._config.idle_block_ble: + logger.info("Disable Bluetooth") + os.system("sudo rfkill block bluetooth") + + def ble_on(self): + if self._config.idle_block_ble: + logger.info("Enable Bluetooth") + os.system("sudo rfkill unblock bluetooth") diff --git a/etrv2mqtt/mqtt.py b/etrv2mqtt/mqtt.py index bff4373..8f62d1b 100644 --- a/etrv2mqtt/mqtt.py +++ b/etrv2mqtt/mqtt.py @@ -38,7 +38,7 @@ def __init__(self, config: Config): def publish_device_data(self, name: str, data: str): if self._client.is_connected(): self._client.publish( - self._config.mqtt.base_topic+'/'+name+'/state', payload=data) + self._config.mqtt.base_topic+'/'+name+'/state', payload=data, retain=self._config.mqtt.device_data_retain) def _publish_autodiscovery_result(self, result: AutodiscoveryResult, retain: bool = False): self._client.publish( diff --git a/etrv2mqtt/schemas/config.schema.json b/etrv2mqtt/schemas/config.schema.json index 86cd086..a9c417a 100644 --- a/etrv2mqtt/schemas/config.schema.json +++ b/etrv2mqtt/schemas/config.schema.json @@ -54,6 +54,11 @@ "default": true, "description": "Set retain bit on autodiscovery related messages" }, + "device_data_retain": { + "type":"boolean", + "default": false, + "description": "Set retain bit on device data messages" + }, "hass_birth_topic": { "type": "string", "default": "hass/status", @@ -71,23 +76,46 @@ "default": {}, "description": "Options common for all thermostats", "properties": { + "poll_schedule": { + "type": "string", + "description": "Polling schedule to use (regular interval, every hour at specified minute)", + "enum": [ "interval", "hour_minute" ], + "default": "interval" + }, "poll_interval": { "type":"integer", - "description": "Interval between thermostat data readouts in seconds", + "description": "Interval between thermostat data readouts in seconds (only used when poll_schedule is interval)", "minimum": 1, "default": 3600 }, + "poll_hour_minute": { + "type": "integer", + "description": "The minute of the hour polling starts (only used when poll_schedule is hour_minute", + "minimum": 0, + "maximum": 59, + "default": 0 + }, "retry_limit": { "type":"integer", "description": "Limit of BLE connect attempts", "minimum": 0, "default": 5 }, + "retry_rerun": { + "type": "boolean", + "description": "Retry failed thermostats once again after the end of the queue", + "default": true + }, "stay_connected": { "type": "boolean", "description": "Set to true in order to leave BLE connection running after polling thermostat data or setting temperature. May drain battery.", "default": false }, + "idle_block_ble": { + "type": "boolean", + "description": "Disable Bluetooth when idle (requires sudo permissions for rfkill) - EXPERIMENTAL, avoid battery draining", + "default": false + }, "report_room_temperature": { "type": "boolean", "description": "Set to false to disable reporting current room temperature as a separate Home Assistant sensor in MQTT auto discovery", @@ -127,4 +155,4 @@ } } } -} \ No newline at end of file +}