Skip to content
This repository has been archived by the owner on Sep 8, 2024. It is now read-only.

Commit

Permalink
Vacation/Away-mode using HVAC Hold mode
Browse files Browse the repository at this point in the history
Supports Manual, Scheduled and Automatic modes
Additionally fixes a few bits of spelling.
  • Loading branch information
mihtjel committed Mar 15, 2021
1 parent 626f6f5 commit 57bb361
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 15 deletions.
12 changes: 8 additions & 4 deletions etrv2mqtt/autodiscovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ class AutodiscoveryResult():

class Autodiscovery():

_termostat_template = json.loads("""
_thermostat_template = json.loads("""
{
"~": "etrv/kitchen",
"name":"Kitchen",
"unique_id":"0000_thermostat",
"temp_cmd_t":"~/set",
"hold_command_topic":"~/set/preset_mode",
"hold_state_topic":"~/state",
"hold_state_template":"{{ value_json.preset_mode }}",
"hold_modes":["Manual","Scheduled","Vacation"],
"temp_cmd_t":"~/set/temperature",
"temp_stat_t":"~/state",
"temp_stat_tpl":"{{ value_json.set_point }}",
"curr_temp_t":"~/state",
Expand Down Expand Up @@ -133,12 +137,12 @@ def _autodiscovery_payload(self, template: dict, dev_mac: str, dev_name: str, se
payload['availability_topic'] = self._config.mqtt.base_topic + "/state"
return payload

def register_termostat(self, dev_name: str, dev_mac: str) -> AutodiscoveryResult:
def register_thermostat(self, dev_name: str, dev_mac: str) -> AutodiscoveryResult:
autodiscovery_topic = self._autodiscovery_topic(
dev_mac, 'climate', 'thermostat')

autodiscovery_msg = self._autodiscovery_payload(
self._termostat_template, dev_mac, dev_name, "Thermostat")
self._thermostat_template, dev_mac, dev_name, "Thermostat")
autodiscovery_msg['~'] = self._config.mqtt.base_topic+'/'+dev_name

return AutodiscoveryResult(autodiscovery_topic, payload=json.dumps(autodiscovery_msg))
Expand Down
46 changes: 42 additions & 4 deletions etrv2mqtt/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def poll(self, mqtt: Mqtt):
def set_temperature(self, mqtt: Mqtt, temperature: float):
pass

@abstractmethod
def set_mode(self, mqtt: Mqtt, mode: bytes):
pass


class TRVDevice(DeviceBase):
def __init__(self, thermostat_config: ThermostatConfig, config: Config):
Expand Down Expand Up @@ -62,6 +66,20 @@ def set_temperature(self, mqtt: Mqtt, temperature: float):
except btle.BTLEDisconnectError as e:
logger.error(e)

def set_mode(self, mqtt: Mqtt, mode: bytes):
try:
logger.info("Setting {} to {}", self._name, mode)

if not self._device.is_connected():
self._device.connect()
eTRVUtils.set_mode(self._device, mode)
# Home assistant needs to see updated settings value to confirm change
self.poll(mqtt)
except btle.BTLEDisconnectError as e:
logger.error(e)
except KeyError as e:
logger.warning("Invalid preset mode: {}", mode)


class DeviceManager():
def __init__(self, config: Config, deviceClass: Type[DeviceBase]):
Expand All @@ -75,6 +93,7 @@ def __init__(self, config: Config, deviceClass: Type[DeviceBase]):

self._mqtt = Mqtt(self._config)
self._mqtt.set_temperature_callback = self._set_temperature_callback
self._mqtt.set_mode_callback = self._set_mode_callback
self._mqtt.hass_birth_callback = self._hass_birth_callback

def _poll_devices(self):
Expand All @@ -99,7 +118,7 @@ def poll_forever(self) -> NoReturn:
mqtt_was_connected = False
time.sleep(2)

def _set_temerature_task(self, device: DeviceBase, temperature: float):
def _set_temperature_task(self, device: DeviceBase, temperature: float):
device.set_temperature(self._mqtt, temperature)
# this will cause the task to be executed only once
return schedule.CancelJob
Expand All @@ -111,12 +130,31 @@ def _set_temperature_callback(self, mqtt: Mqtt, name: str, temperature: float):
return
device = self._devices[name]

# cancel pending temeperature update for the same device
# cancel pending temperature update for the same device
schedule.clear(device)

# schedule temperature update
schedule.every(self._config.setpoint_debounce_time).seconds.do(
self._set_temperature_task, device, temperature).tag(device)

def _set_mode_task(self, device: DeviceBase, mode: bytes):
device.set_mode(self._mqtt, mode)
# this will cause the task to be executed only once
return schedule.CancelJob

def _set_mode_callback(self, mqtt: Mqtt, name: str, mode: bytes):
if name not in self._devices.keys():
logger.warning(
"Device {} not found", name)
return
device = self._devices[name]

# cancel pending updates for the same device
schedule.clear(device)

# schedule temeperature update
# schedule update
schedule.every(self._config.setpoint_debounce_time).seconds.do(
self._set_temerature_task, device, temperature).tag(device)
self._set_mode_task, device, mode).tag(device)

def _hass_birth_callback(self, mqtt: Mqtt):
schedule.run_all(delay_seconds=1)
25 changes: 24 additions & 1 deletion etrv2mqtt/etrvutils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import json
import enum
from dataclasses import dataclass

from libetrv.bluetooth import btle
from libetrv.device import eTRVDevice
from libetrv.data_struct import ScheduleMode
from datetime import datetime

class PresetModes(enum.Enum):
Manual = ScheduleMode.MANUAL
Scheduled = ScheduleMode.SCHEDULED
Vacation = ScheduleMode.VACATION

@dataclass(repr=False)
class eTRVData:
name: str
battery: int
room_temp: float
set_point: float
preset_mode: str
last_update: datetime

def _datetimeconverter(self, o):
Expand All @@ -31,8 +38,24 @@ def create_device(address: str, key: bytes, retry_limit: int = 5) -> eTRVDevice:

@staticmethod
def read_device(device: eTRVDevice) -> eTRVData:
return eTRVData(device.name, device.battery, device.temperature.room_temperature, device.temperature.set_point_temperature, datetime.now())
mode: str
try:
mode = PresetModes(device.settings.schedule_mode).name
except ValueError:
mode = "None"

return eTRVData(device.name,
device.battery,
device.temperature.room_temperature,
device.temperature.set_point_temperature,
mode,
datetime.now())

@staticmethod
def set_temperature(device: eTRVDevice, temperature: float):
device.temperature.set_point_temperature = float(temperature)

@staticmethod
def set_mode(device: eTRVDevice, mode: bytes):
device.settings.schedule_mode = PresetModes[mode.decode('utf-8')].value
device.settings.save()
27 changes: 21 additions & 6 deletions etrv2mqtt/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _on_connect(self, client, userdata, flags, rc):
if self._config.mqtt.autodiscovery:
ad = Autodiscovery(self._config)
for thermostat in self._config.thermostats.values():
self._publish_autodiscovery_result(ad.register_termostat(
self._publish_autodiscovery_result(ad.register_thermostat(
thermostat.topic, thermostat.address), self._config.mqtt.autodiscovery_retain)
self._publish_autodiscovery_result(ad.register_battery(
thermostat.topic, thermostat.address), self._config.mqtt.autodiscovery_retain)
Expand All @@ -68,7 +68,7 @@ def _on_connect(self, client, userdata, flags, rc):

# subscribe to set temperature topics
self._client.subscribe(
self._config.mqtt.base_topic+'/+/set')
self._config.mqtt.base_topic+'/+/set/+')

# subscribe to Home Assistant birth topic
self._client.subscribe(self._config.mqtt.hass_birth_topic)
Expand All @@ -80,7 +80,7 @@ def _on_disconnect(self, client, userdata, rc):
self._is_connected = False

def _on_message(self, client, userdata, msg):
# hass birth message
# Hass birth message
if msg.topic == self._config.mqtt.hass_birth_topic:
try:
# MQTT payload can be random bytes
Expand All @@ -90,9 +90,9 @@ def _on_message(self, client, userdata, msg):
except UnicodeError:
pass

# thermostat set temperature message
elif msg.topic.startswith(self._config.mqtt.base_topic) and msg.topic.endswith('/set'):
name = msg.topic.split('/')[-2]
# Thermostat set temperature message
elif msg.topic.startswith(self._config.mqtt.base_topic) and msg.topic.endswith('/set/temperature'):
name = msg.topic.split('/')[-3]
try:
if self._set_temperature_callback is not None:
self._set_temperature_callback(
Expand All @@ -101,6 +101,13 @@ def _on_message(self, client, userdata, msg):
logger.warning("{}: {} is not a valid float",
name, msg.payload)

# Thermostat set preset mode message
elif msg.topic.startswith(self._config.mqtt.base_topic) and msg.topic.endswith('/set/preset_mode'):
name = msg.topic.split('/')[-3]
if self._set_mode_callback is not None:
self._set_mode_callback(
self, name, msg.payload)

@property
def set_temperature_callback(self) -> Callable[[Mqtt, str, float], None]:
return self._set_temperature_callback
Expand All @@ -109,6 +116,14 @@ def set_temperature_callback(self) -> Callable[[Mqtt, str, float], None]:
def set_temperature_callback(self, callback: Callable[[Mqtt, str, float], None]):
self._set_temperature_callback = callback

@property
def set_mode_callback(self) -> Callable[[Mqtt, str, str], None]:
return self._set_mode_callback

@set_mode_callback.setter
def set_mode_callback(self, callback: Callable[[Mqtt, str, str], None]):
self._set_mode_callback = callback

@property
def hass_birth_callback(self) -> Callable[[Mqtt], None]:
return self._hass_birth_callback
Expand Down

0 comments on commit 57bb361

Please sign in to comment.