diff --git a/Dockerfile b/Dockerfile index 052eac6..809366a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # For more information, please refer to https://aka.ms/vscode-docker-python -FROM python:3.8-slim as base +FROM python:3.9-slim as base FROM base as builder RUN apt-get update \ diff --git a/app.py b/app.py index 48864e6..2180389 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,12 @@ """The main app.""" import logging -from atagmqtt.atag_interaction import AtagInteraction -from atagmqtt.configuration import Settings +import asyncio +from atagmqtt.atag_interaction import main +from atagmqtt.configuration import Settings -LOGGER = logging.getLogger(__name__) logging.basicConfig(level=Settings().loglevel) if __name__ == "__main__": - LOOP = None - INTERACTION = AtagInteraction() - INTERACTION.main() + asyncio.run(main()) + diff --git a/atagmqtt/atag_interaction.py b/atagmqtt/atag_interaction.py index 1f2aaf5..74aedcb 100644 --- a/atagmqtt/atag_interaction.py +++ b/atagmqtt/atag_interaction.py @@ -1,60 +1,50 @@ """Interaction with ATAG ONE.""" +from typing import Optional import asyncio import logging +import aiohttp -from pyatag.gateway import AtagDataStore +from pyatag import AtagException, AtagOne +from pyatag.discovery import async_discover_atag from .device_atagone import DeviceAtagOne from .configuration import Settings - SETTINGS = Settings() LOGGER = logging.getLogger(__name__) -class AtagInteraction: - """Interaction with ATAG ONE as a datastore.""" - def __init__(self): - """Create an interaction with ATAG ONE.""" - self.atag = AtagDataStore(host=SETTINGS.atag_host, hostname=SETTINGS.hostname, paired=True) - self.eventloop = None - self.device = None - - def main(self): - """The main processing function.""" - self.eventloop = None - try: - self.eventloop = asyncio.get_event_loop() - LOGGER.info('Setup connection to ATAG ONE') - self.eventloop.run_until_complete(self.setup()) - LOGGER.info('Starting the main loop for ATAG ONE') - self.eventloop.create_task(self.loop()) - self.eventloop.run_forever() - except KeyboardInterrupt: - LOGGER.info('Closing connection to ATAG ONE') - finally: - LOGGER.info('Cleaning up') - if self.eventloop: - self.eventloop.run_until_complete(self.eventloop.shutdown_asyncgens()) - self.eventloop.close() - self.eventloop = None - - async def setup(self): - """Setup the connection with the ATAG ONE device.""" - if SETTINGS.atag_host: - LOGGER.info(f"Using configured ATAG ONE {SETTINGS.atag_host}") - else: - LOGGER.info(f"Discovering ATAG ONE") - await self.atag.async_host_search() - - await self.atag.async_update() - self.device = DeviceAtagOne(self.atag, name="Atag One", eventloop=self.eventloop) - LOGGER.info(f"Connected to ATAG_ONE device {self.atag.device} @ {self.atag.config.host}") - - async def loop(self): - """The event loop.""" - next_time = self.eventloop.time() +async def main(): + """The main processing function.""" + async with aiohttp.ClientSession() as session: + await run(session) + +async def run(session: aiohttp.ClientSession): + try: + LOGGER.info('Setup connection to ATAG ONE') + device = await setup(session) + LOGGER.info('Starting the main loop for ATAG ONE') while True: - await asyncio.sleep(1) - if self.eventloop.time() > next_time: - await self.device.update() - LOGGER.info('Updated at: {}'.format(self.atag.sensordata['date_time']['state'])) - next_time = self.eventloop.time() + SETTINGS.atag_update_interval + await asyncio.sleep(SETTINGS.atag_update_interval) + await device.update() + LOGGER.info('Updated at: {}'.format(device.atag.report.report_time)) + except AtagException as atag_ex: + LOGGER.error(atag_ex) + return False + except KeyboardInterrupt: + LOGGER.info('Closing connection to ATAG ONE') + +async def setup(session: aiohttp.ClientSession) -> DeviceAtagOne: + + """Setup the connection with the ATAG ONE device.""" + if SETTINGS.atag_host: + LOGGER.info(f"Using configured ATAG ONE {SETTINGS.atag_host}") + else: + LOGGER.info(f"Discovering ATAG ONE") + atag_ip, atag_id = await async_discover_atag() # for auto discovery, requires access to UDP broadcast (hostnet) + SETTINGS.atag_host = atag_ip + + atag = AtagOne(SETTINGS.atag_host, session) + await atag.authorize() + await atag.update(force=True) + device = DeviceAtagOne(atag, asyncio.get_running_loop(), name="Atag One") + LOGGER.info(f"Connected to ATAG ONE device @ {atag.host}") + return device diff --git a/atagmqtt/device_atagone.py b/atagmqtt/device_atagone.py index e653e3e..9c255c7 100644 --- a/atagmqtt/device_atagone.py +++ b/atagmqtt/device_atagone.py @@ -1,5 +1,6 @@ """ATAG ONE device module.""" import logging +import asyncio from homie.device_base import Device_Base from homie.node.node_base import Node_Base @@ -10,7 +11,7 @@ from homie.node.property.property_float import Property_Float from homie.node.property.property_enum import Property_Enum -from pyatag import AtagDataStore +from pyatag import AtagOne from .configuration import Settings @@ -35,12 +36,12 @@ class DeviceAtagOne(Device_Base): """The ATAG ONE device.""" - def __init__(self, atag: AtagDataStore, eventloop, device_id="atagone", name=None): + def __init__(self, atag: AtagOne, eventloop, device_id="atagone", name=None): """Create an ATAG ONE device.""" super().__init__(device_id, name, TRANSLATED_HOMIE_SETTINGS, TRANSLATED_MQTT_SETTINGS) + self.atag: AtagOne = atag + self.temp_unit = atag.climate.temp_unit self._eventloop = eventloop - self.atag: AtagDataStore = atag - self.temp_unit = "°C" node = (Node_Base(self, 'burner', 'Burner', 'status')) self.add_node(node) @@ -62,27 +63,32 @@ def __init__(self, atag: AtagDataStore, eventloop, device_id="atagone", name=Non self.ch_temperature = Property_Temperature( node, id="temperature", name="CH temperature", - settable=False, value=self.atag.temperature) + settable=False, value=self.atag.climate.temperature, + unit=self.temp_unit) node.add_property(self.ch_temperature) self.ch_water_temperature = Property_Temperature( node, id="water-temperature", name="CH water temperature", - settable=False, value=self.atag.sensordata['ch_water_temp'].get('state', 0)) + settable=False, value=self.atag.report["CH Water Temperature"].state, + unit=self.temp_unit) node.add_property(self.ch_water_temperature) self.ch_target_water_temperature = Property_Temperature( node, id="target-water-temperature", name="CH target water temperature", - settable=False, value=self.atag.sensordata['ch_setpoint'].get('state', 0)) + settable=False, value=self.atag.climate.target_temperature, + unit=self.temp_unit) node.add_property(self.ch_target_water_temperature) self.ch_return_water_temperature = Property_Temperature( node, id="return-water-temperature", name="CH return water temperature", - settable=False, value=self.atag.sensordata['ch_return_temp'].get('state', 0)) + settable=False, value=self.atag.report["CH Return Temperature"].state, + unit=self.temp_unit) node.add_property(self.ch_return_water_temperature) self.ch_water_pressure = Property_Float( node, id="water-pressure", name="CH water pressure", - settable=False, value=self.atag.sensordata['ch_water_pres'].get('state', 0)) + settable=False, value=self.atag.report["CH Water Pressure"].state, + unit=self.temp_unit) node.add_property(self.ch_water_pressure) # Domestic hot water status properties @@ -95,7 +101,8 @@ def __init__(self, atag: AtagDataStore, eventloop, device_id="atagone", name=Non self.dhw_temperature = Property_Temperature( node, id="temperature", name="DHW temperature", - settable=False, value=self.atag.dhw_temperature) + settable=False, value=self.atag.dhw.temperature, + unit=self.temp_unit) node.add_property(self.dhw_temperature) node = (Node_Base(self, 'weather', 'Weather', 'status')) @@ -103,7 +110,8 @@ def __init__(self, atag: AtagDataStore, eventloop, device_id="atagone", name=Non self.weather_temperature = Property_Temperature( node, id="temperature", name="Weather temperature", - settable=False, value=self.atag.sensordata['weather_temp'].get('state', 0)) + settable=False, value=self.atag.report["weather_temp"].state, + unit=self.temp_unit) node.add_property(self.weather_temperature) # Control properties @@ -117,91 +125,90 @@ def __init__(self, atag: AtagDataStore, eventloop, device_id="atagone", name=Non node, id='ch-target-temperature', name='CH Target temperature', data_format=ch_target_temperature_limits, unit=self.temp_unit, - value=self.atag.target_temperature, + value=self.atag.climate.target_temperature, set_value=self.set_ch_target_temperature) node.add_property(self.ch_target_temperature) - dhw_min_temp = self.atag.dhw_min_temp - dhw_max_temp = self.atag.dhw_max_temp + dhw_min_temp = self.atag.dhw.min_temp + dhw_max_temp = self.atag.dhw.max_temp dhw_target_temperature_limits = f'{dhw_min_temp}:{dhw_max_temp}' self.dhw_target_temperature = Property_Setpoint( node, id='dhw-target-temperature', name='DHW Target temperature', data_format=dhw_target_temperature_limits, - unit=self.temp_unit, value=self.atag.dhw_target_temperature, + unit=self.temp_unit, value=self.atag.dhw.target_temperature, set_value=self.set_dhw_target_temperature) node.add_property(self.dhw_target_temperature) hvac_values = "auto,heat" self.hvac_mode = Property_Enum( node, id='hvac-mode', name='HVAC mode', - data_format=hvac_values, value=self.atag.hvac_mode, + data_format=hvac_values, + unit=self.temp_unit, + value=self.atag.climate.hvac_mode, set_value=self.set_hvac_mode) node.add_property(self.hvac_mode) - self.start() def set_ch_target_temperature(self, value): """Set target central heating temperature.""" - oldvalue = self.atag.target_temperature + oldvalue = self.atag.climate.target_temperature LOGGER.info(f"Setting target CH temperature from {oldvalue} to {value} {self.temp_unit}") self.ch_target_temperature.value = value - self._eventloop.create_task(self._async_set_ch_target_temperature(value)) + self._run_task(self._async_set_ch_target_temperature(value)) async def _async_set_ch_target_temperature(self, value): - success = await self.atag.set_temp(value) - if success: - LOGGER.info(f"Succeeded setting target CH temperature to {value} {self.temp_unit}") - + await self.atag.climate.set_temp(value) + LOGGER.info(f"Succeeded setting target CH temperature to {value} {self.temp_unit}") + def set_dhw_target_temperature(self, value): """Set target domestic hot water temperature.""" - oldvalue = self.atag.dhw_target_temperature + oldvalue = self.atag.dhw.target_temperature LOGGER.info(f"Setting target DHW temperature from {oldvalue} to {value} {self.temp_unit}") self.dhw_target_temperature.value = value - self._eventloop.create_task(self._async_set_dhw_target_temperature(value)) + self._run_task(self._async_set_dhw_target_temperature(value)) async def _async_set_dhw_target_temperature(self, value): - success = await self.atag.dhw_set_temp(value) - if success: - LOGGER.info(f"Succeeded setting target DHW temperature to {value} {self.temp_unit}") - + await self.atag.dhw.set_temp(value) + LOGGER.info(f"Succeeded setting target DHW temperature to {value} {self.temp_unit}") + def set_hvac_mode(self, value): """Set HVAC mode.""" - oldvalue = self.atag.hvac_mode + oldvalue = self.atag.climate.hvac_mode LOGGER.info(f"Setting HVAC mode from {oldvalue} to {value}") self.hvac_mode.value = value - self._eventloop.create_task(self._async_set_hvac_mode(value)) + self._run_task(self._async_set_hvac_mode(value)) async def _async_set_hvac_mode(self, value): - success = await self.atag.set_hvac_mode(value) - if success: - LOGGER.info(f"Succeeded setting HVAC mode to {value}") - + await self.atag.climate.set_hvac_mode(value) + LOGGER.info(f"Succeeded setting HVAC mode to {value}") + async def update(self): - """Update device status from atag datastorage.""" - await self.atag.async_update() + """Update device status from atag device.""" + await self.atag.update() LOGGER.debug("Updating from latest device report") - self.burner_modulation.value = \ - self.atag.burner_status[1].get('state', 0) if self.atag.burner_status[0] else 0 - self.hvac_mode.value = self.atag.hvac_mode - - self.ch_target_temperature.value = self.atag.target_temperature - self.ch_status.value = "true" if self.atag.ch_status else "false" - self.ch_temperature.value = self.atag.temperature - self.ch_water_temperature.value = self.atag.sensordata['ch_water_temp'].get('state', 0) - self.ch_water_pressure.value = self.atag.sensordata['ch_water_pres'].get('state', 0) - self.ch_target_water_temperature.value = self.atag.sensordata['ch_setpoint'].get('state', 0) - self.ch_return_water_temperature.value = \ - self.atag.sensordata['ch_return_temp'].get('state', 0) - - self.dhw_target_temperature.value = self.atag.dhw_target_temperature - self.dhw_temperature.value = self.atag.dhw_temperature - self.dhw_status.value = "true" if self.atag.dhw_status else "false" - - self.weather_temperature.value = self.atag.sensordata['weather_temp'].get('state', 0) - - if self.atag.dhw_status: + self.burner_modulation.value = self.atag.climate.flame + self.hvac_mode.value = self.atag.climate.hvac_mode + + self.ch_target_temperature.value = self.atag.climate.target_temperature + self.ch_temperature.value = self.atag.climate.temperature + self.ch_target_water_temperature.value = self.atag.climate.target_temperature + self.ch_water_temperature.value = self.atag.report["CH Water Temperature"].state + self.ch_water_pressure.value = self.atag.report["CH Water Pressure"].state + self.ch_return_water_temperature.value = self.atag.report["CH Return Temperature"].state + self.ch_status.value = "true" if self.atag.climate.status else "false" + + self.dhw_target_temperature.value = self.atag.dhw.target_temperature + self.dhw_temperature.value = self.atag.dhw.temperature + self.dhw_status.value = "true" if self.atag.dhw.status else "false" + + self.weather_temperature.value = self.atag.report["weather_temp"].state + + if self.atag.dhw.status: self.burner_target.value = "dhw" - elif self.atag.ch_status: + elif self.atag.climate.status: self.burner_target.value = "ch" else: self.burner_target.value = "none" + + def _run_task(self, coroutine): + asyncio.run_coroutine_threadsafe(coroutine, self._eventloop) diff --git a/requirements.txt b/requirements.txt index 4fcc7b2..e672008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,35 @@ -aiohttp==3.6.2 -asgiref==3.2.3 -astroid==2.3.3 -async-timeout==3.0.1 -atomicwrites==1.3.0 -attrs==19.3.0 -certifi==2019.11.28 -chardet==3.0.4 -colorama==0.4.3 -Homie3==0.3.0 -idna==2.9 -importlib-metadata==1.5.0 -isort==4.3.21 -lazy-object-proxy==1.4.3 -mccabe==0.6.1 -more-itertools==8.2.0 -multidict==4.7.3 -netifaces==0.10.9 -packaging==20.1 -paho-mqtt==1.5.0 -pluggy==0.13.1 -ptvsd==4.3.2 -py==1.8.1 -pydantic==1.4 -pylint==2.4.4 -pytest==5.3.5 -python-dotenv==0.12.0 -rope==0.16.0 -six==1.14.0 -typed-ast==1.4.1 -wcwidth==0.1.8 -wincertstore==0.2 -wrapt==1.11.2 -yarl==1.4.2 -zipp==2.2.0 +aiohttp +asgiref +astroid +async-timeout +atomicwrites +attrs +certifi +chardet +colorama +Homie3 +idna +importlib-metadata +isort +lazy-object-proxy +mccabe +more-itertools +multidict +netifaces +packaging +paho-mqtt +pluggy +ptvsd +py +pydantic +pylint +pytest +python-dotenv +rope +six +typed-ast +wcwidth +wincertstore +wrapt +yarl +zipp