From 6b8b695ba2d00fb529ea2eda24ee17201302191d Mon Sep 17 00:00:00 2001 From: mvn23 Date: Thu, 3 Oct 2019 03:45:54 +0200 Subject: [PATCH 1/4] Add config flow support to opentherm_gw. Bump pyotgw to 0.5b0 (required for connection testing) Existing entries in configuration.yaml will be converted to config entries and ignored in future runs. --- .../opentherm_gw/.translations/en.json | 23 ++++ .../opentherm_gw/.translations/nl.json | 23 ++++ .../components/opentherm_gw/__init__.py | 38 ++++-- .../components/opentherm_gw/binary_sensor.py | 19 ++- .../components/opentherm_gw/climate.py | 14 +- .../components/opentherm_gw/config_flow.py | 124 ++++++++++++++++++ .../components/opentherm_gw/manifest.json | 5 +- .../components/opentherm_gw/sensor.py | 21 ++- .../components/opentherm_gw/strings.json | 23 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- 11 files changed, 249 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/opentherm_gw/.translations/en.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nl.json create mode 100644 homeassistant/components/opentherm_gw/config_flow.py create mode 100644 homeassistant/components/opentherm_gw/strings.json diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000000..65d7d9e92bb1bd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000000..ef3daafe4fe6ee --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway is reeds geconfigureerd", + "id_exists": "Gateway id bestaat reeds", + "serial_error": "Kan niet verbinden met de Gateway", + "timeout": "Time-out van de verbinding" + }, + "step": { + "init": { + "data": { + "device": "Pad of URL", + "floor_temperature": "Thermostaat temperaturen naar beneden afronden", + "id": "ID", + "name": "Naam", + "precision": "Thermostaat temperatuur precisie" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index a32c375ac65258..23c4614637642c 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -6,6 +6,7 @@ import pyotgw.vars as gw_vars import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR @@ -16,13 +17,13 @@ ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, ) -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv @@ -75,28 +76,37 @@ ) -async def async_setup(hass, config): +async def async_setup_entry(hass, config_entry): """Set up the OpenTherm Gateway component.""" - conf = config[DOMAIN] - hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} - for gw_id, cfg in conf.items(): + if DATA_OPENTHERM_GW not in hass.data: + hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} + + for gw_id, cfg in config_entry.data.items(): gateway = OpenThermGatewayDevice(hass, gw_id, cfg) hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway + for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: hass.async_create_task( - async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config) + hass.config_entries.async_forward_entry_setup(config_entry, comp) ) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) register_services(hass) return True +async def async_setup(hass, config): + """Set up the OpenTherm Gateway component.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + conf = config[DOMAIN] + for device_id, device_config in conf.items(): + device_config[CONF_ID] = device_id + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config + ) + ) + return True + + def register_services(hass): """Register services for the component.""" service_reset_schema = vol.Schema( diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 614829265e2dcc..c69d5ebf3e27cb 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -12,18 +12,17 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] - for var, info in BINARY_SENSOR_INFO.items(): - device_class = info[0] - friendly_name_format = info[1] - sensors.append( - OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) - ) + for gw_id, cfg in config_entry.data.items(): + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] + for var, info in BINARY_SENSOR_INFO.items(): + device_class = info[0] + friendly_name_format = info[1] + sensors.append( + OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) + ) async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fab028560bb66e..5165fc67fcfe39 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -33,12 +33,14 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the opentherm_gw device.""" - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] - - gateway = OpenThermClimate(gw_dev) - async_add_entities([gateway]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up an OpenTherm Gateway climate entity.""" + ents = [] + for gw_id, cfg in config_entry.data.items(): + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] + + ents.append(OpenThermClimate(gw_dev)) + async_add_entities(ents) class OpenThermClimate(ClimateDevice): diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py new file mode 100644 index 00000000000000..152e8d4cb705ed --- /dev/null +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -0,0 +1,124 @@ +"""OpenTherm Gateway config flow.""" +import asyncio +from serial import SerialException + +import pyotgw +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.const import ( + CONF_DEVICE, + CONF_ID, + CONF_NAME, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, +) + +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN +from .const import CONF_FLOOR_TEMP, CONF_PRECISION + + +class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """OpenTherm Gateway Config Flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_init(self, info=None): + """Handle config flow initiation.""" + if info: + name = info[CONF_NAME] + device = info[CONF_DEVICE] + gw_id = info.get(CONF_ID, cv.slugify(name)) + precision = info.get(CONF_PRECISION) + floor_temp = info[CONF_FLOOR_TEMP] + + entries = { + k: v + for e in self.hass.config_entries.async_entries(DOMAIN) + for k, v in e.data.items() + } + + if gw_id in entries: + return self._show_form({"base": "id_exists"}) + + if device in [e[CONF_DEVICE] for e in entries.values()]: + return self._show_form({"base": "already_configured"}) + + async def test_connection(): + """Try to connect to the OpenTherm Gateway.""" + gw = pyotgw.pyotgw() + status = await gw.connect(self.hass.loop, device) + await gw.disconnect() + return status.get(pyotgw.OTGW_ABOUT) + + try: + res = await asyncio.wait_for(test_connection(), timeout=10) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout"}) + except SerialException: + return self._show_form({"base": "serial_error"}) + + if res: + return self._create_entry(gw_id, name, device, precision, floor_temp) + + return self._show_form() + + async def async_step_user(self, info=None): + """Handle manual initiation of the config flow.""" + return await self.async_step_init(info) + + async def async_step_import(self, import_config): + """ + Import an OpenTherm Gateway device as a config entry. + + This flow is triggered by `async_setup` for configured devices. + """ + climate_config = import_config.get(CLIMATE_DOMAIN, {}) + formatted_config = { + CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]), + CONF_DEVICE: import_config[CONF_DEVICE], + CONF_ID: import_config[CONF_ID], + CONF_PRECISION: climate_config.get(CONF_PRECISION), + CONF_FLOOR_TEMP: climate_config.get(CONF_FLOOR_TEMP, False), + } + return await self.async_step_init(info=formatted_config) + + def _show_form(self, errors={}): + """Show the config flow form with possible errors.""" + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_DEVICE): str, + vol.Optional(CONF_ID): str, + vol.Optional(CONF_PRECISION): vol.All( + vol.Coerce(float), + vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), + ), + vol.Optional(CONF_FLOOR_TEMP, default=False): bool, + } + ), + errors=errors, + ) + + def _create_entry(self, gw_id, name, device, precision, floor_temp): + """Create entry for the OpenTherm Gateway device.""" + return self.async_create_entry( + title="OpenTherm Gateway", + data={ + gw_id: { + CONF_DEVICE: device, + CONF_NAME: name, + CLIMATE_DOMAIN: { + CONF_PRECISION: precision, + CONF_FLOOR_TEMP: floor_temp, + }, + } + }, + ) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 9c7f165c6dfa19..37d14e6f66f3ce 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,10 +3,11 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.4b4" + "pyotgw==0.5b0" ], "dependencies": [], "codeowners": [ "@mvn23" - ] + ], + "config_flow": true } diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1449caf5defdb9..4f44e3763eaae8 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -12,19 +12,18 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] - for var, info in SENSOR_INFO.items(): - device_class = info[0] - unit = info[1] - friendly_name_format = info[2] - sensors.append( - OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) - ) + for gw_id, cfg in config_entry.data.items(): + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] + for var, info in SENSOR_INFO.items(): + device_class = info[0] + unit = info[1] + friendly_name_format = info[2] + sensors.append( + OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) + ) async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json new file mode 100644 index 00000000000000..a62a462504954c --- /dev/null +++ b/homeassistant/components/opentherm_gw/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "OpenTherm Gateway", + "step": { + "init": { + "title": "OpenTherm Gateway", + "data": { + "name": "Name", + "device": "Path or URL", + "id": "ID", + "precision": "Climate temperature precision", + "floor_temperature": "Floor climate temperature" + } + } + }, + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21f57934e95bd6..2fbc9a3f10f7a3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -44,6 +44,7 @@ "mqtt", "nest", "notion", + "opentherm_gw", "openuv", "owntracks", "plaato", diff --git a/requirements_all.txt b/requirements_all.txt index c84b57a09f36b5..0fc80c77527823 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b4 +pyotgw==0.5b0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From b19367324ca3df678cba669e39d7d3e33055a8e3 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Thu, 3 Oct 2019 10:45:23 +0200 Subject: [PATCH 2/4] Fix not connecting to Gateway on startup. Pylint fixes. --- homeassistant/components/opentherm_gw/__init__.py | 5 +++++ homeassistant/components/opentherm_gw/binary_sensor.py | 2 +- homeassistant/components/opentherm_gw/climate.py | 4 ++-- homeassistant/components/opentherm_gw/config_flow.py | 8 ++++---- homeassistant/components/opentherm_gw/sensor.py | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 23c4614637642c..07ddcf52c800ca 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -84,10 +84,15 @@ async def async_setup_entry(hass, config_entry): for gw_id, cfg in config_entry.data.items(): gateway = OpenThermGatewayDevice(hass, gw_id, cfg) hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway + + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) + for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, comp) ) + register_services(hass) return True diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index c69d5ebf3e27cb..d27a5881ac6cff 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" sensors = [] - for gw_id, cfg in config_entry.data.items(): + for gw_id in config_entry.data: gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 5165fc67fcfe39..adcc9d5c446c96 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an OpenTherm Gateway climate entity.""" ents = [] - for gw_id, cfg in config_entry.data.items(): + for gw_id in config_entry.data: gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] ents.append(OpenThermClimate(gw_dev)) @@ -64,7 +64,7 @@ def __init__(self, gw_dev): async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" - _LOGGER.debug("Added device %s", self.friendly_name) + _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 152e8d4cb705ed..12be32ee15524f 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -51,9 +51,9 @@ async def async_step_init(self, info=None): async def test_connection(): """Try to connect to the OpenTherm Gateway.""" - gw = pyotgw.pyotgw() - status = await gw.connect(self.hass.loop, device) - await gw.disconnect() + otgw = pyotgw.pyotgw() + status = await otgw.connect(self.hass.loop, device) + await otgw.disconnect() return status.get(pyotgw.OTGW_ABOUT) try: @@ -88,7 +88,7 @@ async def async_step_import(self, import_config): } return await self.async_step_init(info=formatted_config) - def _show_form(self, errors={}): + def _show_form(self, errors=None): """Show the config flow form with possible errors.""" return self.async_show_form( step_id="init", diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 4f44e3763eaae8..3ca8fbe4a61908 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" sensors = [] - for gw_id, cfg in config_entry.data.items(): + for gw_id in config_entry.data: gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] for var, info in SENSOR_INFO.items(): device_class = info[0] From fc47dcc120b4ab5408ac9379b01b19185668af02 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 4 Oct 2019 22:58:10 +0200 Subject: [PATCH 3/4] Add tests for config flow. Remove non-essential options from config flow. Restructure config entry data. --- .coveragerc | 5 +- .../components/opentherm_gw/__init__.py | 29 ++-- .../components/opentherm_gw/binary_sensor.py | 19 +- .../components/opentherm_gw/climate.py | 11 +- .../components/opentherm_gw/config_flow.py | 49 +----- .../components/opentherm_gw/const.py | 2 + .../components/opentherm_gw/manifest.json | 2 +- .../components/opentherm_gw/sensor.py | 22 ++- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/opentherm_gw/__init__.py | 1 + .../opentherm_gw/test_config_flow.py | 163 ++++++++++++++++++ 12 files changed, 230 insertions(+), 77 deletions(-) create mode 100644 tests/components/opentherm_gw/__init__.py create mode 100644 tests/components/opentherm_gw/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index aa8f2d8c03d16e..df5b5a2df830ba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -461,7 +461,10 @@ omit = homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py - homeassistant/components/opentherm_gw/* + homeassistant/components/opentherm_gw/__init__.py + homeassistant/components/opentherm_gw/binary_sensor.py + homeassistant/components/opentherm_gw/climate.py + homeassistant/components/opentherm_gw/sensor.py homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 07ddcf52c800ca..ba6de4c0bea6ee 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -37,6 +37,7 @@ CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW, + DOMAIN, SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, @@ -51,8 +52,6 @@ _LOGGER = logging.getLogger(__name__) -DOMAIN = "opentherm_gw" - CLIMATE_SCHEMA = vol.Schema( { vol.Optional(CONF_PRECISION): vol.In( @@ -81,12 +80,11 @@ async def async_setup_entry(hass, config_entry): if DATA_OPENTHERM_GW not in hass.data: hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} - for gw_id, cfg in config_entry.data.items(): - gateway = OpenThermGatewayDevice(hass, gw_id, cfg) - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway + gateway = OpenThermGatewayDevice(hass, config_entry) + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task(gateway.connect_and_subscribe()) for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: hass.async_create_task( @@ -341,20 +339,21 @@ async def set_setback_temp(call): class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" - def __init__(self, hass, gw_id, config): + def __init__(self, hass, config_entry): """Initialize the OpenTherm Gateway.""" self.hass = hass - self.gw_id = gw_id - self.name = config.get(CONF_NAME, gw_id) - self.climate_config = config[CONF_CLIMATE] + self.device_path = config_entry.data[CONF_DEVICE] + self.gw_id = config_entry.data[CONF_ID] + self.name = config_entry.data[CONF_NAME] + self.climate_config = config_entry.options self.status = {} - self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" + self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.gateway = pyotgw.pyotgw() - async def connect_and_subscribe(self, device_path): + async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + await self.gateway.connect(self.hass.loop, self.device_path) + _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) async def cleanup(event): """Reset overrides on the gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index d27a5881ac6cff..36867feda61e52 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id @@ -15,14 +16,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" sensors = [] - for gw_id in config_entry.data: - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] - for var, info in BINARY_SENSOR_INFO.items(): - device_class = info[0] - friendly_name_format = info[1] - sensors.append( - OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) + for var, info in BINARY_SENSOR_INFO.items(): + device_class = info[0] + friendly_name_format = info[1] + sensors.append( + OpenThermBinarySensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + friendly_name_format, ) + ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index adcc9d5c446c96..19763121e89d64 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -17,6 +17,7 @@ ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_ID, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -36,10 +37,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an OpenTherm Gateway climate entity.""" ents = [] - for gw_id in config_entry.data: - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] + ents.append( + OpenThermClimate( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ) + ) - ents.append(OpenThermClimate(gw_dev)) async_add_entities(ents) @@ -50,7 +53,7 @@ def __init__(self, gw_dev): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] + self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP) self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 12be32ee15524f..c618cdae925383 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -6,20 +6,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.const import ( - CONF_DEVICE, - CONF_ID, - CONF_NAME, - PRECISION_HALVES, - PRECISION_TENTHS, - PRECISION_WHOLE, -) +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv from . import DOMAIN -from .const import CONF_FLOOR_TEMP, CONF_PRECISION class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -34,19 +25,13 @@ async def async_step_init(self, info=None): name = info[CONF_NAME] device = info[CONF_DEVICE] gw_id = info.get(CONF_ID, cv.slugify(name)) - precision = info.get(CONF_PRECISION) - floor_temp = info[CONF_FLOOR_TEMP] - entries = { - k: v - for e in self.hass.config_entries.async_entries(DOMAIN) - for k, v in e.data.items() - } + entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] - if gw_id in entries: + if gw_id in [e[CONF_ID] for e in entries]: return self._show_form({"base": "id_exists"}) - if device in [e[CONF_DEVICE] for e in entries.values()]: + if device in [e[CONF_DEVICE] for e in entries]: return self._show_form({"base": "already_configured"}) async def test_connection(): @@ -64,7 +49,7 @@ async def test_connection(): return self._show_form({"base": "serial_error"}) if res: - return self._create_entry(gw_id, name, device, precision, floor_temp) + return self._create_entry(gw_id, name, device) return self._show_form() @@ -78,13 +63,10 @@ async def async_step_import(self, import_config): This flow is triggered by `async_setup` for configured devices. """ - climate_config = import_config.get(CLIMATE_DOMAIN, {}) formatted_config = { CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]), CONF_DEVICE: import_config[CONF_DEVICE], CONF_ID: import_config[CONF_ID], - CONF_PRECISION: climate_config.get(CONF_PRECISION), - CONF_FLOOR_TEMP: climate_config.get(CONF_FLOOR_TEMP, False), } return await self.async_step_init(info=formatted_config) @@ -97,28 +79,13 @@ def _show_form(self, errors=None): vol.Required(CONF_NAME): str, vol.Required(CONF_DEVICE): str, vol.Optional(CONF_ID): str, - vol.Optional(CONF_PRECISION): vol.All( - vol.Coerce(float), - vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), - ), - vol.Optional(CONF_FLOOR_TEMP, default=False): bool, } ), - errors=errors, + errors=errors or {}, ) - def _create_entry(self, gw_id, name, device, precision, floor_temp): + def _create_entry(self, gw_id, name, device): """Create entry for the OpenTherm Gateway device.""" return self.async_create_entry( - title="OpenTherm Gateway", - data={ - gw_id: { - CONF_DEVICE: device, - CONF_NAME: name, - CLIMATE_DOMAIN: { - CONF_PRECISION: precision, - CONF_FLOOR_TEMP: floor_temp, - }, - } - }, + title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} ) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 60042b92867ca4..bd9b372de33200 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -18,6 +18,8 @@ DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_PROBLEM = "problem" +DOMAIN = "opentherm_gw" + SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 37d14e6f66f3ce..a632096cd75e9a 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -10,4 +10,4 @@ "@mvn23" ], "config_flow": true -} +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 3ca8fbe4a61908..c77a73cd18032b 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -15,15 +16,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" sensors = [] - for gw_id in config_entry.data: - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] - for var, info in SENSOR_INFO.items(): - device_class = info[0] - unit = info[1] - friendly_name_format = info[2] - sensors.append( - OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) + for var, info in SENSOR_INFO.items(): + device_class = info[0] + unit = info[1] + friendly_name_format = info[2] + sensors.append( + OpenThermSensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + unit, + friendly_name_format, ) + ) + async_add_entities(sensors) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61d3479d8f62d3..fc41f445dd4b79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,6 +332,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opentherm_gw +pyotgw==0.5b0 + # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e35a83bd24d7ea..930f8848845566 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -137,6 +137,7 @@ "pynws", "pynx584", "pyopenuv", + "pyotgw", "pyotp", "pyps4-homeassistant", "pyqwikswitch", diff --git a/tests/components/opentherm_gw/__init__.py b/tests/components/opentherm_gw/__init__.py new file mode 100644 index 00000000000000..2dfe926765185b --- /dev/null +++ b/tests/components/opentherm_gw/__init__.py @@ -0,0 +1 @@ +"""Tests for the Opentherm Gateway integration.""" diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py new file mode 100644 index 00000000000000..da80e2f9fbb74e --- /dev/null +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the Opentherm Gateway config flow.""" +import asyncio +from serial import SerialException +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME +from homeassistant.components.opentherm_gw.const import DOMAIN + +from pyotgw import OTGW_ABOUT +from tests.common import mock_coro + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Entry 1" + assert result2["data"] == { + CONF_NAME: "Test Entry 1", + CONF_DEVICE: "/dev/ttyUSB0", + CONF_ID: "test_entry_1", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_import(hass): + """Test import from existing config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "legacy_gateway" + assert result["data"] == { + CONF_NAME: "legacy_gateway", + CONF_DEVICE: "/dev/ttyUSB1", + CONF_ID: "legacy_gateway", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_duplicate_entries(hass): + """Test duplicate device or id errors.""" + flow1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result1 = await hass.config_entries.flow.async_configure( + flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + result2 = await hass.config_entries.flow.async_configure( + flow2["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB1"} + ) + result3 = await hass.config_entries.flow.async_configure( + flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} + ) + assert result1["type"] == "create_entry" + assert result2["type"] == "form" + assert result2["errors"] == {"base": "id_exists"} + assert result3["type"] == "form" + assert result3["errors"] == {"base": "already_configured"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_connection_timeout(hass): + """Test we handle connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "timeout"} + assert len(mock_connect.mock_calls) == 1 + + +async def test_form_connection_error(hass): + """Test we handle serial connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "serial_error"} + assert len(mock_connect.mock_calls) == 1 From 0a9a4d6a2df0b7f7a99136a3d6e7e40b3f237fbe Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 5 Oct 2019 01:13:34 +0200 Subject: [PATCH 4/4] Make sure gw_id is slugified --- homeassistant/components/opentherm_gw/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index c618cdae925383..e1b68f1ae4903b 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -24,7 +24,7 @@ async def async_step_init(self, info=None): if info: name = info[CONF_NAME] device = info[CONF_DEVICE] - gw_id = info.get(CONF_ID, cv.slugify(name)) + gw_id = cv.slugify(info.get(CONF_ID, name)) entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)]