From 2f85e9da6032a9a1fff9cd505ef3fa1ba49aa99a Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 28 Jan 2020 09:32:35 +0100 Subject: [PATCH 01/49] add config flow --- homeassistant/components/fritzbox/__init__.py | 80 ++++------- .../components/fritzbox/binary_sensor.py | 35 +++-- homeassistant/components/fritzbox/climate.py | 36 +++-- .../components/fritzbox/config_flow.py | 133 ++++++++++++++++++ homeassistant/components/fritzbox/const.py | 14 ++ .../components/fritzbox/manifest.json | 8 +- homeassistant/components/fritzbox/sensor.py | 42 ++++-- .../components/fritzbox/strings.json | 31 ++++ homeassistant/components/fritzbox/switch.py | 40 ++++-- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + 11 files changed, 338 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/fritzbox/config_flow.py create mode 100644 homeassistant/components/fritzbox/const.py create mode 100644 homeassistant/components/fritzbox/strings.json diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 40aa3a881d12de..753c71fcfdf42a 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,32 +1,11 @@ """Support for AVM Fritz!Box smarthome devices.""" -import logging - -from pyfritzhome import Fritzhome, LoginError +from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import ( - CONF_DEVICES, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] - -DOMAIN = "fritzbox" - -ATTR_STATE_BATTERY_LOW = "battery_low" -ATTR_STATE_DEVICE_LOCKED = "device_locked" -ATTR_STATE_HOLIDAY_MODE = "holiday_mode" -ATTR_STATE_LOCKED = "locked" -ATTR_STATE_SUMMER_MODE = "summer_mode" -ATTR_STATE_WINDOW_OPEN = "window_open" - +from .const import DOMAIN, SUPPORTED_DOMAINS CONFIG_SCHEMA = vol.Schema( { @@ -51,40 +30,35 @@ ) -def setup(hass, config): - """Set up the fritzbox component.""" - - fritz_list = [] - - configured_devices = config[DOMAIN].get(CONF_DEVICES) - for device in configured_devices: - host = device.get(CONF_HOST) - username = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD) - fritzbox = Fritzhome(host=host, user=username, password=password) - try: - fritzbox.login() - _LOGGER.info("Connected to device %s", device) - except LoginError: - _LOGGER.warning("Login to Fritz!Box %s as %s failed", host, username) - continue - - fritz_list.append(fritzbox) +async def async_setup(hass, config): + """Set up the AVM Fritz!Box integration.""" + if DOMAIN in config: + for entry_config in config[DOMAIN][CONF_DEVICES]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=entry_config + ) + ) - if not fritz_list: - _LOGGER.info("No fritzboxes configured") - return False + return True - hass.data[DOMAIN] = fritz_list - def logout_fritzboxes(event): - """Close all connections to the fritzboxes.""" - for fritz in fritz_list: - fritz.logout() +async def async_setup_entry(hass, entry): + """Set up the AVM Fritz!Box platforms.""" + fritz = Fritzhome( + host=entry.data[CONF_HOST], + user=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + fritz.login() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = [] + hass.data[DOMAIN].append(fritz) for domain in SUPPORTED_DOMAINS: - discovery.load_platform(hass, domain, DOMAIN, {}, config) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, domain) + ) return True diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 3d8d676d1d0615..cb2450eb60f4e0 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,17 +1,20 @@ """Support for Fritzbox binary sensors.""" -import logging - import requests from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN as FRITZBOX_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN as FRITZBOX_DOMAIN, LOGGER -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None +): # pragma: no cover """Set up the Fritzbox binary sensor platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Fritzbox binary sensor from config_entry.""" devices = [] fritz_list = hass.data[FRITZBOX_DOMAIN] @@ -21,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_alarm: devices.append(FritzboxBinarySensor(device, fritz)) - add_entities(devices, True) + async_add_entities(devices, True) class FritzboxBinarySensor(BinarySensorDevice): @@ -32,6 +35,22 @@ def __init__(self, device, fritz): self._device = device self._fritz = fritz + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "manufacturer": self._device.manufacturer, + "model": self._device.productname, + "sw_version": self._device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._device.ain + @property def name(self): """Return the name of the entity.""" @@ -54,5 +73,5 @@ def update(self): try: self._device.update() except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Connection error: %s", ex) + LOGGER.warning("Connection error: %s", ex) self._fritz.login() diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 115f7f8e644fda..150dc1a9d710d6 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,6 +1,4 @@ """Support for AVM Fritz!Box smarthome thermostate devices.""" -import logging - import requests from homeassistant.components.climate import ClimateDevice @@ -20,7 +18,7 @@ TEMP_CELSIUS, ) -from . import ( +from .const import ( ATTR_STATE_BATTERY_LOW, ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_HOLIDAY_MODE, @@ -28,10 +26,9 @@ ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, DOMAIN as FRITZBOX_DOMAIN, + LOGGER, ) -_LOGGER = logging.getLogger(__name__) - SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF] @@ -48,8 +45,15 @@ OFF_REPORT_SET_TEMPERATURE = 0.0 -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None +): # pragma: no cover """Set up the Fritzbox smarthome thermostat platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Fritzbox smarthome thermostat from config_entry.""" devices = [] fritz_list = hass.data[FRITZBOX_DOMAIN] @@ -59,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_thermostat: devices.append(FritzboxThermostat(device, fritz)) - add_entities(devices) + async_add_entities(devices) class FritzboxThermostat(ClimateDevice): @@ -74,6 +78,22 @@ def __init__(self, device, fritz): self._comfort_temperature = self._device.comfort_temperature self._eco_temperature = self._device.eco_temperature + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "manufacturer": self._device.manufacturer, + "model": self._device.productname, + "sw_version": self._device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._device.ain + @property def supported_features(self): """Return the list of supported features.""" @@ -205,5 +225,5 @@ def update(self): self._comfort_temperature = self._device.comfort_temperature self._eco_temperature = self._device.eco_temperature except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Fritzbox connection error: %s", ex) + LOGGER.warning("Fritzbox connection error: %s", ex) self._fritz.login() diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py new file mode 100644 index 00000000000000..fe3b29d6386aeb --- /dev/null +++ b/homeassistant/components/fritzbox/config_flow.py @@ -0,0 +1,133 @@ +"""Config flow for AVM Fritz!Box.""" +import socket +from urllib.parse import urlparse + +from pyfritzhome import Fritzhome, LoginError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN + +DATA_SCHEMA_USER = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + +DATA_SCHEMA_CONFIRM = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + +RESULT_AUTH_MISSING = "auth_missing" +RESULT_SUCCESS = "success" +RESULT_NOT_FOUND = "not_found" + + +def _get_ip(host): + if host is None: + return None + return socket.gethostbyname(host) + + +class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a AVM Fritz!Box config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize flow.""" + self._host = None + self._manufacturer = None + self._model = None + self._name = None + self._password = None + self._username = None + + def _get_entry(self): + return self.async_create_entry( + title=self._name, + data={ + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_USERNAME: self._username, + }, + ) + + def _try_connect(self): + """Try to connect and check auth.""" + fritzbox = Fritzhome( + host=self._host, user=self._username, password=self._password + ) + try: + fritzbox.login() + fritzbox.logout() + return RESULT_SUCCESS + except OSError: + return RESULT_NOT_FOUND + except LoginError: + return RESULT_AUTH_MISSING + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + ip_address = await self.hass.async_add_executor_job( + _get_ip, user_input[CONF_HOST] + ) + + await self.async_set_unique_id(ip_address) + self._abort_if_unique_id_configured() + + self._host = user_input[CONF_HOST] + self._name = user_input[CONF_HOST] + self._password = user_input[CONF_PASSWORD] + self._username = user_input[CONF_USERNAME] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result != RESULT_SUCCESS: + return self.async_abort(reason=result) + return self._get_entry() + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER) + + async def async_step_ssdp(self, user_input=None): + """Handle a flow initialized by discovery.""" + host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname + ip_address = await self.hass.async_add_executor_job(_get_ip, host) + + await self.async_set_unique_id(ip_address) + self._abort_if_unique_id_configured() + + self._host = host + self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] + + self.context["title_placeholders"] = {"name": self._name} + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + self._password = user_input[CONF_PASSWORD] + self._username = user_input[CONF_USERNAME] + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_SUCCESS: + return self._get_entry() + + return self.async_show_form( + step_id="confirm", + data_schema=DATA_SCHEMA_CONFIRM, + description_placeholders={"model": self._model}, + ) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py new file mode 100644 index 00000000000000..6b0c9db92b231c --- /dev/null +++ b/homeassistant/components/fritzbox/const.py @@ -0,0 +1,14 @@ +"""Constants for the AVM Fritz!Box integration.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "fritzbox" + +SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] + +ATTR_STATE_BATTERY_LOW = "battery_low" +ATTR_STATE_DEVICE_LOCKED = "device_locked" +ATTR_STATE_HOLIDAY_MODE = "holiday_mode" +ATTR_STATE_LOCKED = "locked" +ATTR_STATE_SUMMER_MODE = "summer_mode" +ATTR_STATE_WINDOW_OPEN = "window_open" diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 494e70e8bccb22..51ba7148606139 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -3,6 +3,12 @@ "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritzbox", "requirements": ["pyfritzhome==0.4.0"], + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:fritzbox:1" + } + ], "dependencies": [], - "codeowners": [] + "codeowners": [], + "config_flow": true } diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 4454ea35bbe816..61cb58492fac96 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,19 +1,27 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" -import logging - import requests from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_LOCKED, + DOMAIN as FRITZBOX_DOMAIN, + LOGGER, +) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None +): # pragma: no cover """Set up the Fritzbox smarthome sensor platform.""" - _LOGGER.debug("Initializing fritzbox temperature sensors") + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Fritzbox smarthome sensor from config_entry.""" + LOGGER.debug("Initializing fritzbox temperature sensors") devices = [] fritz_list = hass.data[FRITZBOX_DOMAIN] @@ -27,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): devices.append(FritzBoxTempSensor(device, fritz)) - add_entities(devices) + async_add_entities(devices) class FritzBoxTempSensor(Entity): @@ -38,6 +46,22 @@ def __init__(self, device, fritz): self._device = device self._fritz = fritz + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "manufacturer": self._device.manufacturer, + "model": self._device.productname, + "sw_version": self._device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._device.ain + @property def name(self): """Return the name of the device.""" @@ -58,7 +82,7 @@ def update(self): try: self._device.update() except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Fritzhome connection error: %s", ex) + LOGGER.warning("Fritzhome connection error: %s", ex) self._fritz.login() @property diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json new file mode 100644 index 00000000000000..4c926f5d39610c --- /dev/null +++ b/homeassistant/components/fritzbox/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "title": "AVM FRITZ!Box", + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "user": { + "title": "AVM FRITZ!Box", + "description": "Enter your AVM FRITZ!Box information.", + "data": { + "host": "Host or IP address", + "username": "Username", + "password": "Password" + } + }, + "confirm": { + "title": "AVM FRITZ!Box", + "description": "Do you want to set up {model}? Manual configurations for this device will be overwritten.", + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "abort": { + "already_in_progress": "AVM FRITZ!Box configuration is already in progress.", + "already_configured": "This AVM FRITZ!Box is already configured.", + "auth_missing": "Home Assistant is not authenticated to connect to this AVM FRITZ!Box.", + "not_found": "No supported AVM FRITZ!Box found on the network." + } + } +} diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index c51c952ab06aba..a78d0ab217b59a 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,14 +1,15 @@ """Support for AVM Fritz!Box smarthome switch devices.""" -import logging - import requests from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS -from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_LOCKED, + DOMAIN as FRITZBOX_DOMAIN, + LOGGER, +) ATTR_TOTAL_CONSUMPTION = "total_consumption" ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" @@ -17,8 +18,15 @@ ATTR_TEMPERATURE_UNIT = "temperature_unit" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None +): # pragma: no cover """Set up the Fritzbox smarthome switch platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Fritzbox smarthome switch from config_entry.""" devices = [] fritz_list = hass.data[FRITZBOX_DOMAIN] @@ -28,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_switch: devices.append(FritzboxSwitch(device, fritz)) - add_entities(devices) + async_add_entities(devices) class FritzboxSwitch(SwitchDevice): @@ -39,6 +47,22 @@ def __init__(self, device, fritz): self._device = device self._fritz = fritz + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "manufacturer": self._device.manufacturer, + "model": self._device.productname, + "sw_version": self._device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._device.ain + @property def available(self): """Return if switch is available.""" @@ -67,7 +91,7 @@ def update(self): try: self._device.update() except requests.exceptions.HTTPError as ex: - _LOGGER.warning("Fritzhome connection error: %s", ex) + LOGGER.warning("Fritzhome connection error: %s", ex) self._fritz.login() @property diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 70fc4355061ab4..ce99a746e99891 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ "elgato", "emulated_roku", "esphome", + "fritzbox", "garmin_connect", "geofency", "geonetnz_quakes", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 83f375f031b987..8ef142bbacdff1 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -11,6 +11,11 @@ "manufacturer": "Royal Philips Electronics" } ], + "fritzbox": [ + { + "st": "urn:schemas-upnp-org:device:fritzbox:1" + } + ], "heos": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" From 0dec687946688541f299b39699bbdead5a6dd90d Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 28 Jan 2020 16:43:51 +0100 Subject: [PATCH 02/49] fix pylint --- homeassistant/components/fritzbox/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index fe3b29d6386aeb..571f09667c147f 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +# pylint:disable=unused-import from .const import DOMAIN DATA_SCHEMA_USER = vol.Schema( @@ -40,8 +41,6 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - def __init__(self): """Initialize flow.""" self._host = None From 6414aeec53f840418ab4395c44cf09de9e3f0798 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 28 Jan 2020 16:44:19 +0100 Subject: [PATCH 03/49] update lib --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 51ba7148606139..1905311a9f65f0 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.4.0"], + "requirements": ["pyfritzhome==0.4.2"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index a53825d083d340..88e94b3721690f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1254,7 +1254,7 @@ pyflunearyou==1.0.3 pyfnip==0.2 # homeassistant.components.fritzbox -pyfritzhome==0.4.0 +pyfritzhome==0.4.2 # homeassistant.components.fronius pyfronius==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b8a64fd24c138..78a92b58329909 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -432,7 +432,7 @@ pyeverlights==0.1.0 pyfido==2.1.1 # homeassistant.components.fritzbox -pyfritzhome==0.4.0 +pyfritzhome==0.4.2 # homeassistant.components.ifttt pyfttt==0.3 From 3a9c02fab582f9b8c171341d9f6859e00b47d496 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 29 Jan 2020 12:39:44 +0100 Subject: [PATCH 04/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 571f09667c147f..3e899847bbc4ca 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -41,6 +41,8 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + def __init__(self): """Initialize flow.""" self._host = None From 469213ec3fc6bd68764e73c09631fa1cb9db8ebb Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 31 Jan 2020 22:11:48 +0100 Subject: [PATCH 05/49] remote devices layer in config --- homeassistant/components/fritzbox/__init__.py | 40 +++++++++++-------- .../components/fritzbox/config_flow.py | 2 +- .../components/fritzbox/strings.json | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 753c71fcfdf42a..d147e98d1e2666 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,29 +1,37 @@ """Support for AVM Fritz!Box smarthome devices.""" +import socket + from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SUPPORTED_DOMAINS + +def ensure_unique_hosts(value): + """Validate that all configs have a unique host.""" + vol.Schema(vol.Unique("duplicate host entries found"))( + [socket.gethostbyname(entry[CONF_HOST]) for entry in value] + ) + return value + + CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICES): vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } - ) - ], + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + } ) - } + ], + ensure_unique_hosts, ) }, extra=vol.ALLOW_EXTRA, @@ -33,7 +41,7 @@ async def async_setup(hass, config): """Set up the AVM Fritz!Box integration.""" if DOMAIN in config: - for entry_config in config[DOMAIN][CONF_DEVICES]: + for entry_config in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=entry_config diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 571f09667c147f..d6acd0890330e3 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -128,5 +128,5 @@ async def async_step_confirm(self, user_input=None): return self.async_show_form( step_id="confirm", data_schema=DATA_SCHEMA_CONFIRM, - description_placeholders={"model": self._model}, + description_placeholders={"name": self._name}, ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 4c926f5d39610c..e3f9e73fd6568a 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -14,7 +14,7 @@ }, "confirm": { "title": "AVM FRITZ!Box", - "description": "Do you want to set up {model}? Manual configurations for this device will be overwritten.", + "description": "Do you want to set up {name}? Manual configurations for this device will be overwritten.", "data": { "username": "Username", "password": "Password" From 890504de625cc6f8fe109f5811547525a3b320df Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 31 Jan 2020 22:27:49 +0100 Subject: [PATCH 06/49] add default host --- homeassistant/components/fritzbox/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index ba8b75ee9d8dd3..0991e6dc5931c6 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -12,9 +12,10 @@ # pylint:disable=unused-import from .const import DOMAIN +DEFAULT_HOST = "fritz.box" DATA_SCHEMA_USER = vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } From 60e97cc67d1047bdbfdb23f3f9e5ba1970a9cf7f Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 1 Feb 2020 21:06:02 +0100 Subject: [PATCH 07/49] avoid double setups of entities --- homeassistant/components/fritzbox/binary_sensor.py | 11 +++++++---- homeassistant/components/fritzbox/climate.py | 10 +++++++--- homeassistant/components/fritzbox/sensor.py | 10 +++++++--- homeassistant/components/fritzbox/switch.py | 11 +++++++---- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index cb2450eb60f4e0..3612e388b893eb 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,10 +1,12 @@ """Support for Fritzbox binary sensors.""" import requests -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from .const import DOMAIN as FRITZBOX_DOMAIN, LOGGER +entities = set() + async def async_setup_platform( hass, config, add_entities, discovery_info=None @@ -21,8 +23,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for fritz in fritz_list: device_list = fritz.get_devices() for device in device_list: - if device.has_alarm: + if device.has_alarm and device.ain not in entities: devices.append(FritzboxBinarySensor(device, fritz)) + entities.add(device.ain) async_add_entities(devices, True) @@ -40,7 +43,7 @@ def device_info(self): """Return device specific attributes.""" return { "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, "manufacturer": self._device.manufacturer, "model": self._device.productname, "sw_version": self._device.fw_version, @@ -49,7 +52,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return self._device.ain + return f"{self._device.ain}-{DOMAIN}" @property def name(self): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 150dc1a9d710d6..4dd1849c92f2b1 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -4,6 +4,7 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, + DOMAIN, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_COMFORT, @@ -44,6 +45,8 @@ ON_REPORT_SET_TEMPERATURE = 30.0 OFF_REPORT_SET_TEMPERATURE = 0.0 +entities = set() + async def async_setup_platform( hass, config, add_entities, discovery_info=None @@ -60,8 +63,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for fritz in fritz_list: device_list = fritz.get_devices() for device in device_list: - if device.has_thermostat: + if device.has_thermostat and device.ain not in entities: devices.append(FritzboxThermostat(device, fritz)) + entities.add(device.ain) async_add_entities(devices) @@ -83,7 +87,7 @@ def device_info(self): """Return device specific attributes.""" return { "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, "manufacturer": self._device.manufacturer, "model": self._device.productname, "sw_version": self._device.fw_version, @@ -92,7 +96,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return self._device.ain + return f"{self._device.ain}-{DOMAIN}" @property def supported_features(self): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 61cb58492fac96..34038266a40b22 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,6 +1,7 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" import requests +from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -11,6 +12,8 @@ LOGGER, ) +entities = set() + async def async_setup_platform( hass, config, add_entities, discovery_info=None @@ -21,7 +24,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" - LOGGER.debug("Initializing fritzbox temperature sensors") devices = [] fritz_list = hass.data[FRITZBOX_DOMAIN] @@ -32,8 +34,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.has_temperature_sensor and not device.has_switch and not device.has_thermostat + and device.ain not in entities ): devices.append(FritzBoxTempSensor(device, fritz)) + entities.add(device.ain) async_add_entities(devices) @@ -51,7 +55,7 @@ def device_info(self): """Return device specific attributes.""" return { "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, "manufacturer": self._device.manufacturer, "model": self._device.productname, "sw_version": self._device.fw_version, @@ -60,7 +64,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return self._device.ain + return f"{self._device.ain}-{DOMAIN}" @property def name(self): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index a78d0ab217b59a..1fd32c5030a81b 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,7 +1,7 @@ """Support for AVM Fritz!Box smarthome switch devices.""" import requests -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from .const import ( @@ -17,6 +17,8 @@ ATTR_TEMPERATURE_UNIT = "temperature_unit" +entities = set() + async def async_setup_platform( hass, config, add_entities, discovery_info=None @@ -33,8 +35,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for fritz in fritz_list: device_list = fritz.get_devices() for device in device_list: - if device.has_switch: + if device.has_switch and device.ain not in entities: devices.append(FritzboxSwitch(device, fritz)) + entities.add(device.ain) async_add_entities(devices) @@ -52,7 +55,7 @@ def device_info(self): """Return device specific attributes.""" return { "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self.unique_id)}, + "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, "manufacturer": self._device.manufacturer, "model": self._device.productname, "sw_version": self._device.fw_version, @@ -61,7 +64,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return self._device.ain + return f"{self._device.ain}-{DOMAIN}" @property def available(self): From fb67cd93c2e78b5f70c0fc9aa9b4c2dcd1dca055 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 2 Feb 2020 08:04:38 +0100 Subject: [PATCH 08/49] remove async_setup_platform --- homeassistant/components/fritzbox/binary_sensor.py | 7 ------- homeassistant/components/fritzbox/climate.py | 7 ------- homeassistant/components/fritzbox/sensor.py | 7 ------- homeassistant/components/fritzbox/switch.py | 7 ------- 4 files changed, 28 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 3612e388b893eb..76e075e1c638c7 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -8,13 +8,6 @@ entities = set() -async def async_setup_platform( - hass, config, add_entities, discovery_info=None -): # pragma: no cover - """Set up the Fritzbox binary sensor platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox binary sensor from config_entry.""" devices = [] diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 4dd1849c92f2b1..7956e45d8e5119 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -48,13 +48,6 @@ entities = set() -async def async_setup_platform( - hass, config, add_entities, discovery_info=None -): # pragma: no cover - """Set up the Fritzbox smarthome thermostat platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome thermostat from config_entry.""" devices = [] diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 34038266a40b22..c72c20c565deef 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -15,13 +15,6 @@ entities = set() -async def async_setup_platform( - hass, config, add_entities, discovery_info=None -): # pragma: no cover - """Set up the Fritzbox smarthome sensor platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" devices = [] diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 1fd32c5030a81b..2a8c639796a49f 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -20,13 +20,6 @@ entities = set() -async def async_setup_platform( - hass, config, add_entities, discovery_info=None -): # pragma: no cover - """Set up the Fritzbox smarthome switch platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" devices = [] From 8744184f6e7d43ee9edd471e43456abcdebfc1ef Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 3 Feb 2020 12:20:15 +0100 Subject: [PATCH 09/49] store entities in hass.data --- homeassistant/components/fritzbox/__init__.py | 13 +++++++++---- homeassistant/components/fritzbox/binary_sensor.py | 6 +++--- homeassistant/components/fritzbox/climate.py | 7 ++++--- homeassistant/components/fritzbox/sensor.py | 7 +++---- homeassistant/components/fritzbox/switch.py | 13 +++++++++---- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index d147e98d1e2666..5c1c7bc99c49d2 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -4,7 +4,13 @@ from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_ENTITIES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SUPPORTED_DOMAINS @@ -60,9 +66,8 @@ async def async_setup_entry(hass, entry): ) fritz.login() - if DOMAIN not in hass.data: - hass.data[DOMAIN] = [] - hass.data[DOMAIN].append(fritz) + hass.data.setdefault(DOMAIN, {CONF_DEVICES: [], CONF_ENTITIES: {}}) + hass.data[DOMAIN][CONF_DEVICES].append(fritz) for domain in SUPPORTED_DOMAINS: hass.async_create_task( diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 76e075e1c638c7..f9724e6e8e0d0e 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -2,16 +2,16 @@ import requests from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.const import CONF_DEVICES, CONF_ENTITIES from .const import DOMAIN as FRITZBOX_DOMAIN, LOGGER -entities = set() - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox binary sensor from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] + fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) for fritz in fritz_list: device_list = fritz.get_devices() diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 7956e45d8e5119..0fce391514b3b2 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -15,6 +15,8 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, + CONF_DEVICES, + CONF_ENTITIES, PRECISION_HALVES, TEMP_CELSIUS, ) @@ -45,13 +47,12 @@ ON_REPORT_SET_TEMPERATURE = 30.0 OFF_REPORT_SET_TEMPERATURE = 0.0 -entities = set() - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome thermostat from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] + fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) for fritz in fritz_list: device_list = fritz.get_devices() diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index c72c20c565deef..8e9a1590bb9eec 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -2,7 +2,7 @@ import requests from homeassistant.components.sensor import DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONF_DEVICES, CONF_ENTITIES, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from .const import ( @@ -12,13 +12,12 @@ LOGGER, ) -entities = set() - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] + fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) for fritz in fritz_list: device_list = fritz.get_devices() diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 2a8c639796a49f..6b94fdb2da526d 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -2,7 +2,13 @@ import requests from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_DEVICES, + CONF_ENTITIES, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, +) from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -17,13 +23,12 @@ ATTR_TEMPERATURE_UNIT = "temperature_unit" -entities = set() - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN] + fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) for fritz in fritz_list: device_list = fritz.get_devices() From 999d28a55447f6eb248949e919df6537439f9a56 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 3 Feb 2020 14:27:55 +0100 Subject: [PATCH 10/49] pass fritz connection together with config_entry --- homeassistant/components/fritzbox/__init__.py | 11 ++------ .../components/fritzbox/binary_sensor.py | 15 ++++------ homeassistant/components/fritzbox/climate.py | 18 +++++------- homeassistant/components/fritzbox/sensor.py | 28 +++++++++---------- homeassistant/components/fritzbox/switch.py | 24 ++++++---------- 5 files changed, 36 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 5c1c7bc99c49d2..56f27b09100292 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -4,13 +4,7 @@ from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import ( - CONF_DEVICES, - CONF_ENTITIES, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SUPPORTED_DOMAINS @@ -66,8 +60,7 @@ async def async_setup_entry(hass, entry): ) fritz.login() - hass.data.setdefault(DOMAIN, {CONF_DEVICES: [], CONF_ENTITIES: {}}) - hass.data[DOMAIN][CONF_DEVICES].append(fritz) + entry.data["fritz"] = fritz for domain in SUPPORTED_DOMAINS: hass.async_create_task( diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index f9724e6e8e0d0e..965dc5c409cc7f 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -2,7 +2,6 @@ import requests from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from homeassistant.const import CONF_DEVICES, CONF_ENTITIES from .const import DOMAIN as FRITZBOX_DOMAIN, LOGGER @@ -10,15 +9,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox binary sensor from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) + device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) + fritz = config_entry.data["fritz"] - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if device.has_alarm and device.ain not in entities: - devices.append(FritzboxBinarySensor(device, fritz)) - entities.add(device.ain) + for device in fritz.get_devices(): + if device.has_alarm and device.ain not in device_ids: + devices.append(FritzboxBinarySensor(device, fritz)) + device_ids.add(device.ain) async_add_entities(devices, True) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 0fce391514b3b2..c8d0ea63ac5251 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -15,8 +15,6 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, - CONF_DEVICES, - CONF_ENTITIES, PRECISION_HALVES, TEMP_CELSIUS, ) @@ -51,15 +49,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome thermostat from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) - - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if device.has_thermostat and device.ain not in entities: - devices.append(FritzboxThermostat(device, fritz)) - entities.add(device.ain) + device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) + fritz = config_entry.data["fritz"] + + for device in fritz.get_devices(): + if device.has_thermostat and device.ain not in device_ids: + devices.append(FritzboxThermostat(device, fritz)) + device_ids.add(device.ain) async_add_entities(devices) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 8e9a1590bb9eec..134447d5724659 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -2,7 +2,7 @@ import requests from homeassistant.components.sensor import DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_ENTITIES, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from .const import ( @@ -16,20 +16,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) - - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if ( - device.has_temperature_sensor - and not device.has_switch - and not device.has_thermostat - and device.ain not in entities - ): - devices.append(FritzBoxTempSensor(device, fritz)) - entities.add(device.ain) + device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) + fritz = config_entry.data["fritz"] + + for device in fritz.get_devices(): + if ( + device.has_temperature_sensor + and not device.has_switch + and not device.has_thermostat + and device.ain not in device_ids + ): + devices.append(FritzBoxTempSensor(device, fritz)) + device_ids.add(device.ain) async_add_entities(devices) diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 6b94fdb2da526d..4a9269cb29b53e 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -2,13 +2,7 @@ import requests from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_DEVICES, - CONF_ENTITIES, - ENERGY_KILO_WATT_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -27,15 +21,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" devices = [] - fritz_list = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - entities = hass.data[FRITZBOX_DOMAIN][CONF_ENTITIES].setdefault(DOMAIN, set()) - - for fritz in fritz_list: - device_list = fritz.get_devices() - for device in device_list: - if device.has_switch and device.ain not in entities: - devices.append(FritzboxSwitch(device, fritz)) - entities.add(device.ain) + device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) + fritz = config_entry.data["fritz"] + + for device in fritz.get_devices(): + if device.has_switch and device.ain not in device_ids: + devices.append(FritzboxSwitch(device, fritz)) + device_ids.add(device.ain) async_add_entities(devices) From af95de97d398b3c7e4625c6b77ec19f9e7ecb4ee Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 3 Feb 2020 14:47:12 +0100 Subject: [PATCH 11/49] fritz connections try no4 (or is it even more) --- homeassistant/components/fritzbox/__init__.py | 7 ++++--- homeassistant/components/fritzbox/binary_sensor.py | 7 ++++--- homeassistant/components/fritzbox/climate.py | 6 ++++-- homeassistant/components/fritzbox/const.py | 1 + homeassistant/components/fritzbox/sensor.py | 7 ++++--- homeassistant/components/fritzbox/switch.py | 12 +++++++++--- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 56f27b09100292..016b747c23c1b6 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -4,10 +4,10 @@ from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, SUPPORTED_DOMAINS +from .const import CONF_CONNECTIONS, DOMAIN, SUPPORTED_DOMAINS def ensure_unique_hosts(value): @@ -60,7 +60,8 @@ async def async_setup_entry(hass, entry): ) fritz.login() - entry.data["fritz"] = fritz + hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) + hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz for domain in SUPPORTED_DOMAINS: hass.async_create_task( diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 965dc5c409cc7f..1fb8aa62634816 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -2,15 +2,16 @@ import requests from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.const import CONF_DEVICES -from .const import DOMAIN as FRITZBOX_DOMAIN, LOGGER +from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox binary sensor from config_entry.""" devices = [] - device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) - fritz = config_entry.data["fritz"] + device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): if device.has_alarm and device.ain not in device_ids: diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index c8d0ea63ac5251..1484b87e4e9784 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, + CONF_DEVICES, PRECISION_HALVES, TEMP_CELSIUS, ) @@ -26,6 +27,7 @@ ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, + CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER, ) @@ -49,8 +51,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome thermostat from config_entry.""" devices = [] - device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) - fritz = config_entry.data["fritz"] + device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): if device.has_thermostat and device.ain not in device_ids: diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 6b0c9db92b231c..7a6d849794113e 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -5,6 +5,7 @@ DOMAIN = "fritzbox" SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] +CONF_CONNECTIONS = "connections" ATTR_STATE_BATTERY_LOW = "battery_low" ATTR_STATE_DEVICE_LOCKED = "device_locked" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 134447d5724659..d22ba525435ae7 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -2,12 +2,13 @@ import requests from homeassistant.components.sensor import DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, + CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER, ) @@ -16,8 +17,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" devices = [] - device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) - fritz = config_entry.data["fritz"] + device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): if ( diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 4a9269cb29b53e..b8e715bffcedf3 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -2,11 +2,17 @@ import requests from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_DEVICES, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, +) from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, + CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER, ) @@ -21,8 +27,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" devices = [] - device_ids = hass.data.setdefault(FRITZBOX_DOMAIN, set()) - fritz = config_entry.data["fritz"] + device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): if device.has_switch and device.ain not in device_ids: From 0800e0806871e305d0460ef67caf906e645d0cc3 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 4 Feb 2020 14:12:22 +0100 Subject: [PATCH 12/49] fix comments --- homeassistant/components/fritzbox/__init__.py | 2 +- .../components/fritzbox/binary_sensor.py | 16 ++++++++-------- homeassistant/components/fritzbox/climate.py | 15 +++++++-------- homeassistant/components/fritzbox/config_flow.py | 12 +++--------- homeassistant/components/fritzbox/const.py | 14 ++++++++------ homeassistant/components/fritzbox/sensor.py | 15 +++++++-------- homeassistant/components/fritzbox/switch.py | 16 ++++++++-------- 7 files changed, 42 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 016b747c23c1b6..b0f492218e37c2 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass, entry): user=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ) - fritz.login() + await hass.async_add_executor_job(fritz.login()) hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1fb8aa62634816..6f7ab25b8ed3aa 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Fritzbox binary sensors.""" import requests -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_DEVICES from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER @@ -9,16 +9,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox binary sensor from config_entry.""" - devices = [] - device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = [] + devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): - if device.has_alarm and device.ain not in device_ids: - devices.append(FritzboxBinarySensor(device, fritz)) - device_ids.add(device.ain) + if device.has_alarm and device.ain not in devices: + entities.append(FritzboxBinarySensor(device, fritz)) + devices.add(device.ain) - async_add_entities(devices, True) + async_add_entities(entities, True) class FritzboxBinarySensor(BinarySensorDevice): @@ -43,7 +43,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return f"{self._device.ain}-{DOMAIN}" + return self._device.ain @property def name(self): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 1484b87e4e9784..d0e91853f59fb6 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -4,7 +4,6 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, - DOMAIN, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_COMFORT, @@ -50,16 +49,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome thermostat from config_entry.""" - devices = [] - device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = [] + devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): - if device.has_thermostat and device.ain not in device_ids: - devices.append(FritzboxThermostat(device, fritz)) - device_ids.add(device.ain) + if device.has_thermostat and device.ain not in devices: + entities.append(FritzboxThermostat(device, fritz)) + devices.add(device.ain) - async_add_entities(devices) + async_add_entities(entities) class FritzboxThermostat(ClimateDevice): @@ -88,7 +87,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return f"{self._device.ain}-{DOMAIN}" + return self._device.ain @property def supported_features(self): diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 0991e6dc5931c6..4000e33443f9d2 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -30,12 +30,6 @@ RESULT_NOT_FOUND = "not_found" -def _get_ip(host): - if host is None: - return None - return socket.gethostbyname(host) - - class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a AVM Fritz!Box config flow.""" @@ -85,7 +79,7 @@ async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if user_input is not None: ip_address = await self.hass.async_add_executor_job( - _get_ip, user_input[CONF_HOST] + socket.gethostbyname, user_input[CONF_HOST] ) await self.async_set_unique_id(ip_address) @@ -104,10 +98,10 @@ async def async_step_user(self, user_input=None): return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER) - async def async_step_ssdp(self, user_input=None): + async def async_step_ssdp(self, user_input): """Handle a flow initialized by discovery.""" host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname - ip_address = await self.hass.async_add_executor_job(_get_ip, host) + ip_address = await self.hass.async_add_executor_job(socket.gethostbyname, host) await self.async_set_unique_id(ip_address) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 7a6d849794113e..4ba829f51de6ca 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -1,15 +1,17 @@ """Constants for the AVM Fritz!Box integration.""" import logging -LOGGER = logging.getLogger(__package__) -DOMAIN = "fritzbox" - -SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] -CONF_CONNECTIONS = "connections" - ATTR_STATE_BATTERY_LOW = "battery_low" ATTR_STATE_DEVICE_LOCKED = "device_locked" ATTR_STATE_HOLIDAY_MODE = "holiday_mode" ATTR_STATE_LOCKED = "locked" ATTR_STATE_SUMMER_MODE = "summer_mode" ATTR_STATE_WINDOW_OPEN = "window_open" + +CONF_CONNECTIONS = "connections" + +DOMAIN = "fritzbox" + +LOGGER = logging.getLogger(__package__) + +SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index d22ba525435ae7..1c6b4cc3f7daea 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,7 +1,6 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" import requests -from homeassistant.components.sensor import DOMAIN from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -16,8 +15,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome sensor from config_entry.""" - devices = [] - device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = [] + devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): @@ -25,12 +24,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.has_temperature_sensor and not device.has_switch and not device.has_thermostat - and device.ain not in device_ids + and device.ain not in devices ): - devices.append(FritzBoxTempSensor(device, fritz)) - device_ids.add(device.ain) + entities.append(FritzBoxTempSensor(device, fritz)) + devices.add(device.ain) - async_add_entities(devices) + async_add_entities(entities) class FritzBoxTempSensor(Entity): @@ -55,7 +54,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return f"{self._device.ain}-{DOMAIN}" + return self._device.ain @property def name(self): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index b8e715bffcedf3..328abec0d198c3 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,7 +1,7 @@ """Support for AVM Fritz!Box smarthome switch devices.""" import requests -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICES, @@ -26,16 +26,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" - devices = [] - device_ids = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] + entities = [] + devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] for device in fritz.get_devices(): - if device.has_switch and device.ain not in device_ids: - devices.append(FritzboxSwitch(device, fritz)) - device_ids.add(device.ain) + if device.has_switch and device.ain not in devices: + entities.append(FritzboxSwitch(device, fritz)) + devices.add(device.ain) - async_add_entities(devices) + async_add_entities(entities) class FritzboxSwitch(SwitchDevice): @@ -60,7 +60,7 @@ def device_info(self): @property def unique_id(self): """Return the unique ID of the device.""" - return f"{self._device.ain}-{DOMAIN}" + return self._device.ain @property def available(self): From cb1ac6efb4f99c4d42a9ebeb15b31726b460e2bc Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 4 Feb 2020 14:42:34 +0100 Subject: [PATCH 13/49] add unloading --- homeassistant/components/fritzbox/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index b0f492218e37c2..5fe41727276e39 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass, entry): user=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ) - await hass.async_add_executor_job(fritz.login()) + await hass.async_add_executor_job(fritz.login) hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz @@ -69,3 +69,9 @@ async def async_setup_entry(hass, entry): ) return True + + +async def async_unload_entry(hass, entry): + """Unloading the AVM Fritz!Box platforms.""" + fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] + return await hass.async_add_executor_job(fritz.logout) From fbe1fbb7c150b5549e0936512808888bbbd3dd88 Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 7 Feb 2020 22:07:30 +0100 Subject: [PATCH 14/49] fixed comments --- homeassistant/components/fritzbox/__init__.py | 4 ++- .../components/fritzbox/config_flow.py | 28 ++++++++++++++----- .../components/fritzbox/strings.json | 6 ++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 5fe41727276e39..27df1a6e247356 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -74,4 +74,6 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unloading the AVM Fritz!Box platforms.""" fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] - return await hass.async_add_executor_job(fritz.logout) + await hass.async_add_executor_job(fritz.logout) + + return True diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 4000e33443f9d2..0546b76acd9a0e 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -25,9 +25,9 @@ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) -RESULT_AUTH_MISSING = "auth_missing" -RESULT_SUCCESS = "success" +RESULT_AUTH_FAILED = "auth_failed" RESULT_NOT_FOUND = "not_found" +RESULT_SUCCESS = "success" class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -69,7 +69,7 @@ def _try_connect(self): except OSError: return RESULT_NOT_FOUND except LoginError: - return RESULT_AUTH_MISSING + return RESULT_AUTH_FAILED async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" @@ -77,6 +77,8 @@ async def async_step_import(self, user_input=None): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: ip_address = await self.hass.async_add_executor_job( socket.gethostbyname, user_input[CONF_HOST] @@ -92,11 +94,16 @@ async def async_step_user(self, user_input=None): result = await self.hass.async_add_executor_job(self._try_connect) - if result != RESULT_SUCCESS: + if result == RESULT_AUTH_FAILED: + errors["base"] = result + elif result != RESULT_SUCCESS: return self.async_abort(reason=result) - return self._get_entry() + else: + return self._get_entry() - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER) + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors + ) async def async_step_ssdp(self, user_input): """Handle a flow initialized by discovery.""" @@ -114,16 +121,23 @@ async def async_step_ssdp(self, user_input): async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" + errors = {} + if user_input is not None: self._password = user_input[CONF_PASSWORD] self._username = user_input[CONF_USERNAME] result = await self.hass.async_add_executor_job(self._try_connect) - if result == RESULT_SUCCESS: + if result == RESULT_AUTH_FAILED: + errors["base"] = result + elif result != RESULT_SUCCESS: + return self.async_abort(reason=result) + else: return self._get_entry() return self.async_show_form( step_id="confirm", data_schema=DATA_SCHEMA_CONFIRM, description_placeholders={"name": self._name}, + errors=errors, ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index e3f9e73fd6568a..94b798e640b2cd 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -14,7 +14,7 @@ }, "confirm": { "title": "AVM FRITZ!Box", - "description": "Do you want to set up {name}? Manual configurations for this device will be overwritten.", + "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", "data": { "username": "Username", "password": "Password" @@ -24,8 +24,10 @@ "abort": { "already_in_progress": "AVM FRITZ!Box configuration is already in progress.", "already_configured": "This AVM FRITZ!Box is already configured.", - "auth_missing": "Home Assistant is not authenticated to connect to this AVM FRITZ!Box.", "not_found": "No supported AVM FRITZ!Box found on the network." + }, + "error": { + "auth_failed": "Username and/or password are incorrect." } } } From 8bf928356c8e841d2a026879835b5a1349b06b78 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 13 Feb 2020 08:06:12 +0100 Subject: [PATCH 15/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 0546b76acd9a0e..b6345d86e31fc8 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -94,12 +94,11 @@ async def async_step_user(self, user_input=None): result = await self.hass.async_add_executor_job(self._try_connect) - if result == RESULT_AUTH_FAILED: - errors["base"] = result - elif result != RESULT_SUCCESS: - return self.async_abort(reason=result) - else: + if result == RESULT_SUCCESS: return self._get_entry() + elif result != RESULT_AUTH_FAILED: + return self.async_abort(reason=result) + errors["base"] = result return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors From 76d71120d22391d44aab325ebf1fb8ed472456ef Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 12:55:02 +0100 Subject: [PATCH 16/49] Update const.py --- homeassistant/components/fritzbox/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 4ba829f51de6ca..1e3c254a06a004 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -10,6 +10,9 @@ CONF_CONNECTIONS = "connections" +DEFAULT_HOST = "fritz.box" +DEFAULT_USERNAME = "admin" + DOMAIN = "fritzbox" LOGGER = logging.getLogger(__package__) From 8786b38224bfdd46539c43e9bc5e0f1f2a0815ac Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 12:55:47 +0100 Subject: [PATCH 17/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index b6345d86e31fc8..2b74827eb03f53 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -10,13 +10,12 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME # pylint:disable=unused-import -from .const import DOMAIN +from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN -DEFAULT_HOST = "fritz.box" DATA_SCHEMA_USER = vol.Schema( { vol.Required(CONF_HOST, default=DEFAULT_HOST): str, - vol.Required(CONF_USERNAME): str, + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, vol.Required(CONF_PASSWORD): str, } ) From 64ba364a0604aadd67b126ddeaeec5a37bbfa30d Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 12:56:45 +0100 Subject: [PATCH 18/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 27df1a6e247356..da72d9d6f2687b 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -7,7 +7,13 @@ from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from .const import CONF_CONNECTIONS, DOMAIN, SUPPORTED_DOMAINS +from .const import ( + CONF_CONNECTIONS, + DEFAULT_HOST, + DEFAULT_USERNAME, + DOMAIN, + SUPPORTED_DOMAINS, +) def ensure_unique_hosts(value): @@ -25,8 +31,10 @@ def ensure_unique_hosts(value): [ vol.Schema( { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required( + CONF_PASSWORD, default=DEFAULT_USERNAME + ): cv.string, vol.Required(CONF_USERNAME): cv.string, } ) From 60278ab5aed042c5b40733ebcd4082d25599bfce Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 12:58:01 +0100 Subject: [PATCH 19/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 2b74827eb03f53..1ef787c3f87f04 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -21,7 +21,10 @@ ) DATA_SCHEMA_CONFIRM = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } ) RESULT_AUTH_FAILED = "auth_failed" From 428cebab4b0769204231b25291d1395286336363 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 13:01:38 +0100 Subject: [PATCH 20/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index da72d9d6f2687b..966045d271a725 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -32,10 +32,10 @@ def ensure_unique_hosts(value): vol.Schema( { vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Required( - CONF_PASSWORD, default=DEFAULT_USERNAME + CONF_USERNAME, default=DEFAULT_USERNAME ): cv.string, - vol.Required(CONF_USERNAME): cv.string, } ) ], From cabe57fbeb92bbef90d8bb2b943b6fe6a45c2c61 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 13:07:10 +0100 Subject: [PATCH 21/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 966045d271a725..8a887a10f568ae 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -56,6 +56,14 @@ async def async_setup(hass, config): ) ) + def logout_fritzboxes(event): + """Close all connections to the fritzboxes.""" + if DOMAIN in hass.data and CONF_CONNECTIONS in hass.data[DOMAIN]: + for fritz in hass.data[DOMAIN][CONF_CONNECTIONS]: + fritz.logout() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) + return True From 16853b64ad92b398a47ea045e8dfbcf0c0dd818c Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 13:09:25 +0100 Subject: [PATCH 22/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 1ef787c3f87f04..b894cb164a60f9 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -86,7 +86,7 @@ async def async_step_user(self, user_input=None): socket.gethostbyname, user_input[CONF_HOST] ) - await self.async_set_unique_id(ip_address) + await self.async_set_unique_id(ip_address, raise_on_progress=False) self._abort_if_unique_id_configured() self._host = user_input[CONF_HOST] From 9a1a6cdb36f8800d2634f7893d5421ddc49f1b21 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 17 Feb 2020 14:03:49 +0100 Subject: [PATCH 23/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 8a887a10f568ae..ee06a4e4711de9 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -4,7 +4,13 @@ from pyfritzhome import Fritzhome import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv from .const import ( From a62a7254f649dc9edd9d05bd54214153027192f0 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 18 Feb 2020 07:50:05 +0100 Subject: [PATCH 24/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index ee06a4e4711de9..d573ed5a126577 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -62,14 +62,6 @@ async def async_setup(hass, config): ) ) - def logout_fritzboxes(event): - """Close all connections to the fritzboxes.""" - if DOMAIN in hass.data and CONF_CONNECTIONS in hass.data[DOMAIN]: - for fritz in hass.data[DOMAIN][CONF_CONNECTIONS]: - fritz.logout() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) - return True @@ -90,6 +82,12 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, domain) ) + def logout_fritzbox(event): + """Close connections to this fritzbox.""" + fritz.logout() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + return True From 431f2305d98364520f7d2b151ea234f23bd77e00 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 18 Feb 2020 12:02:26 +0100 Subject: [PATCH 25/49] Update __init__.py --- homeassistant/components/fritzbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index d573ed5a126577..194ab48d9a3700 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -86,7 +86,7 @@ def logout_fritzbox(event): """Close connections to this fritzbox.""" fritz.logout() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) return True From 48d24e69f82dcc7839fbd8efb3ade3c61fc94f24 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 19 Feb 2020 15:28:27 +0100 Subject: [PATCH 26/49] Update config_flow.py --- homeassistant/components/fritzbox/config_flow.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index b894cb164a60f9..5d8bcccd88559f 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -98,7 +98,7 @@ async def async_step_user(self, user_input=None): if result == RESULT_SUCCESS: return self._get_entry() - elif result != RESULT_AUTH_FAILED: + if result != RESULT_AUTH_FAILED: return self.async_abort(reason=result) errors["base"] = result @@ -129,12 +129,11 @@ async def async_step_confirm(self, user_input=None): self._username = user_input[CONF_USERNAME] result = await self.hass.async_add_executor_job(self._try_connect) - if result == RESULT_AUTH_FAILED: - errors["base"] = result - elif result != RESULT_SUCCESS: - return self.async_abort(reason=result) - else: + if result == RESULT_SUCCESS: return self._get_entry() + if result != RESULT_AUTH_FAILED: + return self.async_abort(reason=result) + errors["base"] = result return self.async_show_form( step_id="confirm", From 1447914221bbebeb7dd0831a4223942c1456a113 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 27 Feb 2020 23:18:02 +0100 Subject: [PATCH 27/49] add init tests --- .coveragerc | 1 - tests/components/fritzbox/test_init.py | 76 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/components/fritzbox/test_init.py diff --git a/.coveragerc b/.coveragerc index fe5242327dcf06..aae1fc42728043 100644 --- a/.coveragerc +++ b/.coveragerc @@ -244,7 +244,6 @@ omit = homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/* homeassistant/components/fritz/device_tracker.py - homeassistant/components/fritzbox/* homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py homeassistant/components/fronius/sensor.py diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py new file mode 100644 index 00000000000000..5b2976f4a4c996 --- /dev/null +++ b/tests/components/fritzbox/test_init.py @@ -0,0 +1,76 @@ +"""Tests for the AVM Fritz!Box Integration.""" +from unittest.mock import Mock, call, patch + +import pytest + +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +ENTITY_ID = f"{SWITCH_DOMAIN}.fritzbox" +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def test_setup(hass: HomeAssistantType, fritz: Mock): + """Test setup of integration.""" + await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + entries = hass.config_entries.async_entries() + assert entries + assert entries[0].data[CONF_HOST] == "fake_host" + assert entries[0].data[CONF_PASSWORD] == "fake_pass" + assert entries[0].data[CONF_USERNAME] == "fake_user" + assert fritz.call_count == 1 + assert fritz.call_args_list == [ + call(host="fake_host", password="fake_pass", user="fake_user") + ] + + +async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): + """Test duplicate config of integration.""" + DUPLICATE = {FB_DOMAIN: [MOCK_CONFIG[FB_DOMAIN][0], MOCK_CONFIG[FB_DOMAIN][0]]} + await async_setup_component(hass, FB_DOMAIN, DUPLICATE) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID) is None + assert len(hass.states.async_all()) == 0 + assert "duplicate host entries found" in caplog.text + + +async def test_setup_duplicate_entries(hass: HomeAssistantType, fritz: Mock): + """Test duplicate setup of integration.""" + await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries()) == 1 + await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_unload(hass: HomeAssistantType, fritz: Mock): + """Test unload of integration.""" + await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + # how to call unloading? + assert fritz.logout.call_count >= 1 From 8ddf34812afb91a94ae781531f5086d1d4e64c80 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 12 Mar 2020 13:33:38 +0100 Subject: [PATCH 28/49] test unloading --- homeassistant/components/fritzbox/__init__.py | 3 ++ tests/components/fritzbox/__init__.py | 15 +++++++ tests/components/fritzbox/test_init.py | 43 +++++++++++++++---- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 194ab48d9a3700..09f20d83886e1a 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -96,4 +96,7 @@ async def async_unload_entry(hass, entry): fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] await hass.async_add_executor_job(fritz.logout) + for domain in SUPPORTED_DOMAINS: + await hass.config_entries.async_forward_entry_unload(entry, domain) + return True diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 24bb7c3a1810e3..82ba25350ef13b 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1 +1,16 @@ """Tests for the FritzBox! integration.""" +from unittest.mock import Mock + + +class FritzDeviceSwitchMock(Mock): + """Mock of a AVM Fritz!Box switch device.""" + + ain = "fake_ain" + energy = 1234 + has_alarm = False + has_switch = True + has_temperature_sensor = False + has_thermostat = False + name = "fake_name" + power = 5678 + present = True diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 5b2976f4a4c996..6b9845a2d2df8d 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -5,11 +5,15 @@ from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -ENTITY_ID = f"{SWITCH_DOMAIN}.fritzbox" +from . import FritzDeviceSwitchMock + +from tests.common import MockConfigEntry + MOCK_CONFIG = { FB_DOMAIN: [ { @@ -36,7 +40,7 @@ def fritz_fixture(): async def test_setup(hass: HomeAssistantType, fritz: Mock): """Test setup of integration.""" - await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() entries = hass.config_entries.async_entries() assert entries @@ -52,25 +56,46 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): """Test duplicate config of integration.""" DUPLICATE = {FB_DOMAIN: [MOCK_CONFIG[FB_DOMAIN][0], MOCK_CONFIG[FB_DOMAIN][0]]} - await async_setup_component(hass, FB_DOMAIN, DUPLICATE) + assert await async_setup_component(hass, FB_DOMAIN, DUPLICATE) is False await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID) is None + assert len(hass.states.async_entity_ids()) == 0 assert len(hass.states.async_all()) == 0 assert "duplicate host entries found" in caplog.text async def test_setup_duplicate_entries(hass: HomeAssistantType, fritz: Mock): """Test duplicate setup of integration.""" - await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) is True await hass.async_block_till_done() assert len(hass.config_entries.async_entries()) == 1 - await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) is True assert len(hass.config_entries.async_entries()) == 1 async def test_unload(hass: HomeAssistantType, fritz: Mock): """Test unload of integration.""" - await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] + entity_id = f"{SWITCH_DOMAIN}.fake_name" + + entry = MockConfigEntry( + domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][0], unique_id=entity_id, + ) + entry.add_to_hass(hass) + + config_entries = hass.config_entries.async_entries(FB_DOMAIN) + assert len(config_entries) == 1 + assert entry is config_entries[0] + + assert await async_setup_component(hass, FB_DOMAIN, {}) is True await hass.async_block_till_done() - # how to call unloading? - assert fritz.logout.call_count >= 1 + + assert entry.state == ENTRY_STATE_LOADED + state = hass.states.get(entity_id) + assert state + + await hass.config_entries.async_unload(entry.entry_id) + + assert fritz().logout.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + state = hass.states.get(entity_id) + assert state is None From 995632fd26ef8ca5d53cc1c9e5356ad6b13e7999 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 12 Mar 2020 22:26:50 +0100 Subject: [PATCH 29/49] add switch tests --- homeassistant/components/fritzbox/const.py | 5 + homeassistant/components/fritzbox/switch.py | 7 +- tests/components/fritzbox/__init__.py | 6 +- tests/components/fritzbox/test_switch.py | 125 ++++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 tests/components/fritzbox/test_switch.py diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 1e3c254a06a004..53a53a78ab0022 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -8,6 +8,11 @@ ATTR_STATE_SUMMER_MODE = "summer_mode" ATTR_STATE_WINDOW_OPEN = "window_open" +ATTR_TEMPERATURE_UNIT = "temperature_unit" + +ATTR_TOTAL_CONSUMPTION = "total_consumption" +ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" + CONF_CONNECTIONS = "connections" DEFAULT_HOST = "fritz.box" diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 5a2b9b7ce68687..d17003b7b6d017 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -12,17 +12,16 @@ from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, + ATTR_TEMPERATURE_UNIT, + ATTR_TOTAL_CONSUMPTION, + ATTR_TOTAL_CONSUMPTION_UNIT, CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER, ) -ATTR_TOTAL_CONSUMPTION = "total_consumption" -ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR -ATTR_TEMPERATURE_UNIT = "temperature_unit" - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Fritzbox smarthome switch from config_entry.""" diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 82ba25350ef13b..bebac8ea68b978 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -6,11 +6,15 @@ class FritzDeviceSwitchMock(Mock): """Mock of a AVM Fritz!Box switch device.""" ain = "fake_ain" + device_lock = True energy = 1234 has_alarm = False has_switch = True - has_temperature_sensor = False + has_temperature_sensor = True has_thermostat = False + is_on = True + lock = True name = "fake_name" power = 5678 present = True + temperature = 135 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py new file mode 100644 index 00000000000000..78d4b52870d312 --- /dev/null +++ b/tests/components/fritzbox/test_switch.py @@ -0,0 +1,125 @@ +"""Tests for samsungtv component.""" +from datetime import timedelta + +from asynctest import mock +from asynctest.mock import Mock, patch +import pytest +from requests.exceptions import HTTPError + +from homeassistant.components.fritzbox.const import ( + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_LOCKED, + ATTR_TEMPERATURE_UNIT, + ATTR_TOTAL_CONSUMPTION, + ATTR_TOTAL_CONSUMPTION_UNIT, + DOMAIN as FB_DOMAIN, +) +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_TEMPERATURE, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + ENERGY_KILO_WATT_HOUR, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + TEMP_CELSIUS, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import FritzDeviceSwitchMock + +from tests.common import async_fire_time_changed + +ENTITY_ID = f"{DOMAIN}.fake_name" +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def setup_fritzbox(hass: HomeAssistantType, config: dict): + """Set up mock Samsung TV.""" + assert await async_setup_component(hass, FB_DOMAIN, config) is True + await hass.async_block_till_done() + + +async def test_setup(hass: HomeAssistantType, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceSwitchMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_STATE_DEVICE_LOCKED] is True + assert state.attributes[ATTR_STATE_LOCKED] is True + assert state.attributes[ATTR_TEMPERATURE] == "135" + assert state.attributes[ATTR_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert state.attributes[ATTR_TOTAL_CONSUMPTION] == "1.234" + assert state.attributes[ATTR_TOTAL_CONSUMPTION_UNIT] == ENERGY_KILO_WATT_HOUR + + +async def test_turn_on(hass: HomeAssistantType, fritz: Mock): + """Test turn device on.""" + device = FritzDeviceSwitchMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_switch_state_on.call_count == 1 + + +async def test_turn_off(hass: HomeAssistantType, fritz: Mock): + """Test turn device off.""" + device = FritzDeviceSwitchMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_switch_state_off.call_count == 1 + + +async def test_update_error(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceSwitchMock() + device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert fritz().login.call_count == 1 + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert fritz().login.call_count == 2 From 2cbea8c1565665dc95ae0d1fe2c609f95b35d442 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 12 Mar 2020 22:56:27 +0100 Subject: [PATCH 30/49] add sensor tests --- tests/components/fritzbox/__init__.py | 43 +++++++- .../components/fritzbox/test_binary_sensor.py | 94 ++++++++++++++++ tests/components/fritzbox/test_sensor.py | 101 ++++++++++++++++++ tests/components/fritzbox/test_switch.py | 16 ++- 4 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 tests/components/fritzbox/test_binary_sensor.py create mode 100644 tests/components/fritzbox/test_sensor.py diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index bebac8ea68b978..46cdcb8d77c8f7 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -2,19 +2,56 @@ from unittest.mock import Mock +class FritzDeviceBinarySensorMock(Mock): + """Mock of a AVM Fritz!Box binary sensor device.""" + + ain = "fake_ain" + alert_state = "fake_state" + fw_version = "1.2.3" + has_alarm = True + has_switch = False + has_temperature_sensor = False + has_thermostat = False + manufacturer = "fake_manufacturer" + name = "fake_name" + present = True + productname = "fake_productname" + + +class FritzDeviceSensorMock(Mock): + """Mock of a AVM Fritz!Box sensor device.""" + + ain = "fake_ain" + device_lock = "fake_locked_device" + fw_version = "1.2.3" + has_alarm = False + has_switch = False + has_temperature_sensor = True + has_thermostat = False + lock = "fake_locked" + manufacturer = "fake_manufacturer" + name = "fake_name" + present = True + productname = "fake_productname" + temperature = 1.23 + + class FritzDeviceSwitchMock(Mock): """Mock of a AVM Fritz!Box switch device.""" ain = "fake_ain" - device_lock = True + device_lock = "fake_locked_device" energy = 1234 + fw_version = "1.2.3" has_alarm = False has_switch = True has_temperature_sensor = True has_thermostat = False - is_on = True - lock = True + switch_state = "fake_state" + lock = "fake_locked" + manufacturer = "fake_manufacturer" name = "fake_name" power = 5678 present = True + productname = "fake_productname" temperature = 135 diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py new file mode 100644 index 00000000000000..061ae93a673f45 --- /dev/null +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -0,0 +1,94 @@ +"""Tests for samsungtv component.""" +from datetime import timedelta + +from asynctest import mock +from asynctest.mock import Mock, patch +import pytest +from requests.exceptions import HTTPError + +from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import FritzDeviceBinarySensorMock + +from tests.common import async_fire_time_changed + +ENTITY_ID = f"{DOMAIN}.fake_name" +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def setup_fritzbox(hass: HomeAssistantType, config: dict): + """Set up mock Samsung TV.""" + assert await async_setup_component(hass, FB_DOMAIN, config) is True + await hass.async_block_till_done() + + +async def test_setup(hass: HomeAssistantType, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceBinarySensorMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.state == "on" + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_DEVICE_CLASS] == "window" + + +async def test_update(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceBinarySensorMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert device.update.call_count == 1 + + +async def test_update_error(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceBinarySensorMock() + device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert fritz().login.call_count == 1 + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py new file mode 100644 index 00000000000000..0209752b5921b9 --- /dev/null +++ b/tests/components/fritzbox/test_sensor.py @@ -0,0 +1,101 @@ +"""Tests for samsungtv component.""" +from datetime import timedelta + +from asynctest import mock +from asynctest.mock import Mock, patch +import pytest +from requests.exceptions import HTTPError + +from homeassistant.components.fritzbox.const import ( + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_LOCKED, + DOMAIN as FB_DOMAIN, +) +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import FritzDeviceSensorMock + +from tests.common import async_fire_time_changed + +ENTITY_ID = f"{DOMAIN}.fake_name" +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def setup_fritzbox(hass: HomeAssistantType, config: dict): + """Set up mock Samsung TV.""" + assert await async_setup_component(hass, FB_DOMAIN, config) is True + await hass.async_block_till_done() + + +async def test_setup(hass: HomeAssistantType, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceSensorMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.state == "1.23" + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" + assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + + +async def test_update(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceSensorMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert device.update.call_count == 1 + + +async def test_update_error(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceSensorMock() + device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert fritz().login.call_count == 1 + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 78d4b52870d312..a1b389643f8c19 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -77,8 +77,8 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" - assert state.attributes[ATTR_STATE_DEVICE_LOCKED] is True - assert state.attributes[ATTR_STATE_LOCKED] is True + assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" + assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" assert state.attributes[ATTR_TEMPERATURE] == "135" assert state.attributes[ATTR_TEMPERATURE_UNIT] == TEMP_CELSIUS assert state.attributes[ATTR_TOTAL_CONSUMPTION] == "1.234" @@ -111,6 +111,18 @@ async def test_turn_off(hass: HomeAssistantType, fritz: Mock): assert device.set_switch_state_off.call_count == 1 +async def test_update(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceSwitchMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + assert device.update.call_count == 1 + + async def test_update_error(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() From 26fb29c8ec4d78e82f6d5d36d44bf0c620f7621e Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 7 Apr 2020 14:45:20 +0200 Subject: [PATCH 31/49] add climate tests --- tests/components/fritzbox/__init__.py | 29 +- .../components/fritzbox/test_binary_sensor.py | 21 +- tests/components/fritzbox/test_climate.py | 297 +++++++++--------- tests/components/fritzbox/test_init.py | 4 +- tests/components/fritzbox/test_sensor.py | 22 +- tests/components/fritzbox/test_switch.py | 22 +- 6 files changed, 223 insertions(+), 172 deletions(-) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 46cdcb8d77c8f7..f0144132982993 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the FritzBox! integration.""" +"""Tests for the AVM Fritz!Box integration.""" from unittest.mock import Mock @@ -18,6 +18,33 @@ class FritzDeviceBinarySensorMock(Mock): productname = "fake_productname" +class FritzDeviceClimateMock(Mock): + """Mock of a AVM Fritz!Box climate device.""" + + actual_temperature = 18.0 + ain = "fake_ain" + alert_state = "fake_state" + battery_level = 23 + battery_low = True + comfort_temperature = 22.0 + device_lock = "fake_locked_device" + eco_temperature = 16.0 + fw_version = "1.2.3" + has_alarm = False + has_switch = False + has_temperature_sensor = False + has_thermostat = True + holiday_active = "fake_holiday" + lock = "fake_locked" + manufacturer = "fake_manufacturer" + name = "fake_name" + present = True + productname = "fake_productname" + summer_active = "fake_summer" + target_temperature = 19.5 + window_open = "fake_window" + + class FritzDeviceSensorMock(Mock): """Mock of a AVM Fritz!Box sensor device.""" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 061ae93a673f45..acf35fc5886d4f 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Tests for samsungtv component.""" +"""Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta from asynctest import mock @@ -49,7 +49,7 @@ def fritz_fixture(): async def setup_fritzbox(hass: HomeAssistantType, config: dict): - """Set up mock Samsung TV.""" + """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) is True await hass.async_block_till_done() @@ -75,9 +75,15 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): await setup_fritzbox(hass, MOCK_CONFIG) - next_update = dt_util.utcnow() + timedelta(seconds=20) - async_fire_time_changed(hass, next_update) assert device.update.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 2 + assert fritz().login.call_count == 1 async def test_update_error(hass: HomeAssistantType, fritz: Mock): @@ -88,7 +94,12 @@ async def test_update_error(hass: HomeAssistantType, fritz: Mock): await setup_fritzbox(hass, MOCK_CONFIG) + assert device.update.call_count == 1 assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=20) + + next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 2 assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index b535b35e182c7d..75f22219a715d7 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,150 +1,147 @@ -"""The tests for the demo climate component.""" -import unittest -from unittest.mock import Mock, patch - -import requests - -from homeassistant.components.fritzbox.climate import FritzboxThermostat - - -class TestFritzboxClimate(unittest.TestCase): - """Test Fritz!Box heating thermostats.""" - - def setUp(self): - """Create a mock device to test on.""" - self.device = Mock() - self.device.name = "Test Thermostat" - self.device.actual_temperature = 18.0 - self.device.target_temperature = 19.5 - self.device.comfort_temperature = 22.0 - self.device.eco_temperature = 16.0 - self.device.present = True - self.device.device_lock = True - self.device.lock = False - self.device.battery_low = True - self.device.set_target_temperature = Mock() - self.device.update = Mock() - mock_fritz = Mock() - mock_fritz.login = Mock() - self.thermostat = FritzboxThermostat(self.device, mock_fritz) - - def test_init(self): - """Test instance creation.""" - assert 18.0 == self.thermostat._current_temperature - assert 19.5 == self.thermostat._target_temperature - assert 22.0 == self.thermostat._comfort_temperature - assert 16.0 == self.thermostat._eco_temperature - - def test_supported_features(self): - """Test supported features property.""" - assert self.thermostat.supported_features == 17 - - def test_available(self): - """Test available property.""" - assert self.thermostat.available - self.thermostat._device.present = False - assert not self.thermostat.available - - def test_name(self): - """Test name property.""" - assert "Test Thermostat" == self.thermostat.name - - def test_temperature_unit(self): - """Test temperature_unit property.""" - assert "°C" == self.thermostat.temperature_unit - - def test_precision(self): - """Test precision property.""" - assert 0.5 == self.thermostat.precision - - def test_current_temperature(self): - """Test current_temperature property incl. special temperatures.""" - assert 18 == self.thermostat.current_temperature - - def test_target_temperature(self): - """Test target_temperature property.""" - assert 19.5 == self.thermostat.target_temperature - - self.thermostat._target_temperature = 126.5 - assert self.thermostat.target_temperature == 0.0 - - self.thermostat._target_temperature = 127.0 - assert self.thermostat.target_temperature == 30.0 - - @patch.object(FritzboxThermostat, "set_hvac_mode") - def test_set_temperature_operation_mode(self, mock_set_op): - """Test set_temperature by operation_mode.""" - self.thermostat.set_temperature(hvac_mode="heat") - mock_set_op.assert_called_once_with("heat") - - def test_set_temperature_temperature(self): - """Test set_temperature by temperature.""" - self.thermostat.set_temperature(temperature=23.0) - self.thermostat._device.set_target_temperature.assert_called_once_with(23.0) - - @patch.object(FritzboxThermostat, "set_hvac_mode") - def test_set_temperature_none(self, mock_set_op): - """Test set_temperature with no arguments.""" - self.thermostat.set_temperature() - mock_set_op.assert_not_called() - self.thermostat._device.set_target_temperature.assert_not_called() - - @patch.object(FritzboxThermostat, "set_hvac_mode") - def test_set_temperature_operation_mode_precedence(self, mock_set_op): - """Test set_temperature for precedence of operation_mode argument.""" - self.thermostat.set_temperature(hvac_mode="heat", temperature=23.0) - mock_set_op.assert_called_once_with("heat") - self.thermostat._device.set_target_temperature.assert_not_called() - - def test_hvac_mode(self): - """Test operation mode property for different temperatures.""" - self.thermostat._target_temperature = 127.0 - assert "heat" == self.thermostat.hvac_mode - self.thermostat._target_temperature = 126.5 - assert "off" == self.thermostat.hvac_mode - self.thermostat._target_temperature = 22.0 - assert "heat" == self.thermostat.hvac_mode - self.thermostat._target_temperature = 16.0 - assert "heat" == self.thermostat.hvac_mode - self.thermostat._target_temperature = 12.5 - assert "heat" == self.thermostat.hvac_mode - - def test_operation_list(self): - """Test operation_list property.""" - assert ["heat", "off"] == self.thermostat.hvac_modes - - def test_min_max_temperature(self): - """Test min_temp and max_temp properties.""" - assert 8.0 == self.thermostat.min_temp - assert 28.0 == self.thermostat.max_temp - - def test_device_state_attributes(self): - """Test device_state property.""" - attr = self.thermostat.device_state_attributes - assert attr["device_locked"] is True - assert attr["locked"] is False - assert attr["battery_low"] is True - - def test_update(self): - """Test update function.""" - device = Mock() - device.update = Mock() - device.actual_temperature = 10.0 - device.target_temperature = 11.0 - device.comfort_temperature = 12.0 - device.eco_temperature = 13.0 - self.thermostat._device = device - - self.thermostat.update() - - device.update.assert_called_once_with() - assert 10.0 == self.thermostat._current_temperature - assert 11.0 == self.thermostat._target_temperature - assert 12.0 == self.thermostat._comfort_temperature - assert 13.0 == self.thermostat._eco_temperature - - def test_update_http_error(self): - """Test exception handling of update function.""" - self.device.update.side_effect = requests.exceptions.HTTPError - self.thermostat.update() - self.thermostat._fritz.login.assert_called_once_with() +"""Tests for AVM Fritz!Box climate component.""" +from datetime import timedelta + +from asynctest.mock import Mock, patch +import pytest +from requests.exceptions import HTTPError + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DOMAIN, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, +) +from homeassistant.components.fritzbox.const import ( + ATTR_STATE_BATTERY_LOW, + ATTR_STATE_DEVICE_LOCKED, + ATTR_STATE_HOLIDAY_MODE, + ATTR_STATE_LOCKED, + ATTR_STATE_SUMMER_MODE, + ATTR_STATE_WINDOW_OPEN, + DOMAIN as FB_DOMAIN, +) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_FRIENDLY_NAME, + ATTR_TEMPERATURE, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import FritzDeviceClimateMock + +from tests.common import async_fire_time_changed + +ENTITY_ID = f"{DOMAIN}.fake_name" +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def setup_fritzbox(hass: HomeAssistantType, config: dict): + """Set up mock AVM Fritz!Box.""" + assert await async_setup_component(hass, FB_DOMAIN, config) is True + await hass.async_block_till_done() + + +async def test_setup(hass: HomeAssistantType, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.attributes[ATTR_BATTERY_LEVEL] == 23 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT, HVAC_MODE_OFF] + assert state.attributes[ATTR_MAX_TEMP] == 28 + assert state.attributes[ATTR_MIN_TEMP] == 8 + assert state.attributes[ATTR_PRESET_MODE] is None + assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] + assert state.attributes[ATTR_STATE_BATTERY_LOW] is True + assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday" + assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" + assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer" + assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window" + assert state.attributes[ATTR_TEMPERATURE] == 19.5 + assert state.state == HVAC_MODE_HEAT + + +async def test_update(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18 + assert state.attributes[ATTR_MAX_TEMP] == 28 + assert state.attributes[ATTR_MIN_TEMP] == 8 + assert state.attributes[ATTR_TEMPERATURE] == 19.5 + + device.actual_temperature = 19 + device.target_temperature = 20 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + + assert device.update.call_count == 1 + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19 + assert state.attributes[ATTR_TEMPERATURE] == 20 + + +async def test_update_error(hass: HomeAssistantType, fritz: Mock): + """Test update with error.""" + device = FritzDeviceClimateMock() + device.update.side_effect = HTTPError("Boom") + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + assert device.update.call_count == 0 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 1 + assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 6b9845a2d2df8d..9b912507cdeff0 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,4 +1,4 @@ -"""Tests for the AVM Fritz!Box Integration.""" +"""Tests for the AVM Fritz!Box integration.""" from unittest.mock import Mock, call, patch import pytest @@ -56,7 +56,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): """Test duplicate config of integration.""" DUPLICATE = {FB_DOMAIN: [MOCK_CONFIG[FB_DOMAIN][0], MOCK_CONFIG[FB_DOMAIN][0]]} - assert await async_setup_component(hass, FB_DOMAIN, DUPLICATE) is False + assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 0 assert len(hass.states.async_all()) == 0 diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 0209752b5921b9..9c8c019dc46337 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,7 +1,6 @@ -"""Tests for samsungtv component.""" +"""Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta -from asynctest import mock from asynctest.mock import Mock, patch import pytest from requests.exceptions import HTTPError @@ -54,7 +53,7 @@ def fritz_fixture(): async def setup_fritzbox(hass: HomeAssistantType, config: dict): - """Set up mock Samsung TV.""" + """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) is True await hass.async_block_till_done() @@ -81,21 +80,30 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) + assert device.update.call_count == 0 + assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=20) + next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert device.update.call_count == 1 + assert fritz().login.call_count == 1 async def test_update_error(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" device = FritzDeviceSensorMock() - device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] + device.update.side_effect = HTTPError("Boom") fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - + assert device.update.call_count == 0 assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=20) + + next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 1 assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index a1b389643f8c19..e76a6ca744c4d3 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,7 +1,6 @@ -"""Tests for samsungtv component.""" +"""Tests for AVM Fritz!Box switch component.""" from datetime import timedelta -from asynctest import mock from asynctest.mock import Mock, patch import pytest from requests.exceptions import HTTPError @@ -61,7 +60,7 @@ def fritz_fixture(): async def setup_fritzbox(hass: HomeAssistantType, config: dict): - """Set up mock Samsung TV.""" + """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) is True await hass.async_block_till_done() @@ -117,21 +116,30 @@ async def test_update(hass: HomeAssistantType, fritz: Mock): fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) + assert device.update.call_count == 0 + assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=20) + next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert device.update.call_count == 1 + assert fritz().login.call_count == 1 async def test_update_error(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() - device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")] + device.update.side_effect = HTTPError("Boom") fritz().get_devices.return_value = [device] await setup_fritzbox(hass, MOCK_CONFIG) - + assert device.update.call_count == 0 assert fritz().login.call_count == 1 - next_update = dt_util.utcnow() + timedelta(seconds=20) + + next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 1 assert fritz().login.call_count == 2 From f15511ec237d02660ba665a0a751142c40c1ec99 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 7 Apr 2020 15:16:37 +0200 Subject: [PATCH 32/49] test target temperature --- tests/components/fritzbox/test_climate.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 75f22219a715d7..8c48b1269b8e69 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -101,6 +101,30 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): assert state.state == HVAC_MODE_HEAT +async def test_target_temperature_on(hass: HomeAssistantType, fritz: Mock): + """Test turn device on.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + device.target_temperature = 127.0 + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_TEMPERATURE] == 30 + + +async def test_target_temperature_off(hass: HomeAssistantType, fritz: Mock): + """Test turn device on.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + device.target_temperature = 126.5 + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_TEMPERATURE] == 0 + + async def test_update(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() From 5187baca6abca82afd00d018f6d3b4d183bcfc1c Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 14 Apr 2020 13:10:32 +0200 Subject: [PATCH 33/49] mock config to package --- tests/components/fritzbox/__init__.py | 13 ++++++++++++ .../components/fritzbox/test_binary_sensor.py | 19 ++---------------- tests/components/fritzbox/test_climate.py | 20 ++----------------- tests/components/fritzbox/test_init.py | 12 +---------- tests/components/fritzbox/test_sensor.py | 14 +------------ tests/components/fritzbox/test_switch.py | 14 +------------ 6 files changed, 20 insertions(+), 72 deletions(-) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f0144132982993..c79f2fc1f47f5e 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,6 +1,19 @@ """Tests for the AVM Fritz!Box integration.""" from unittest.mock import Mock +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +MOCK_CONFIG = { + FB_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] +} + class FritzDeviceBinarySensorMock(Mock): """Mock of a AVM Fritz!Box binary sensor device.""" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index acf35fc5886d4f..8f1abe13071fb9 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -8,31 +8,16 @@ from homeassistant.components.binary_sensor import DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import FritzDeviceBinarySensorMock +from . import MOCK_CONFIG, FritzDeviceBinarySensorMock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -MOCK_CONFIG = { - FB_DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] -} @pytest.fixture(name="fritz") diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 8c48b1269b8e69..f4bb29b4d95d32 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -27,32 +27,16 @@ ATTR_STATE_WINDOW_OPEN, DOMAIN as FB_DOMAIN, ) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_FRIENDLY_NAME, - ATTR_TEMPERATURE, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import FritzDeviceClimateMock +from . import MOCK_CONFIG, FritzDeviceClimateMock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -MOCK_CONFIG = { - FB_DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] -} @pytest.fixture(name="fritz") diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 9b912507cdeff0..d8ac66c7c02a47 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -10,20 +10,10 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from . import FritzDeviceSwitchMock +from . import MOCK_CONFIG, FritzDeviceSwitchMock from tests.common import MockConfigEntry -MOCK_CONFIG = { - FB_DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] -} - @pytest.fixture(name="fritz") def fritz_fixture(): diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 9c8c019dc46337..72f7939c14c4ca 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -14,29 +14,17 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, TEMP_CELSIUS, ) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import FritzDeviceSensorMock +from . import MOCK_CONFIG, FritzDeviceSensorMock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -MOCK_CONFIG = { - FB_DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] -} @pytest.fixture(name="fritz") diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index e76a6ca744c4d3..f231fc133e2d4e 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -18,9 +18,6 @@ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, ENERGY_KILO_WATT_HOUR, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -30,20 +27,11 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import FritzDeviceSwitchMock +from . import MOCK_CONFIG, FritzDeviceSwitchMock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" -MOCK_CONFIG = { - FB_DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] -} @pytest.fixture(name="fritz") From c8cc9eef5e069dc7a76b6091eb9441d3ac69ebfb Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 14 Apr 2020 13:13:49 +0200 Subject: [PATCH 34/49] comments --- tests/components/fritzbox/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index d8ac66c7c02a47..b44fcd5d7ce8f1 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -48,8 +48,8 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl DUPLICATE = {FB_DOMAIN: [MOCK_CONFIG[FB_DOMAIN][0], MOCK_CONFIG[FB_DOMAIN][0]]} assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 0 - assert len(hass.states.async_all()) == 0 + assert not hass.states.async_entity_ids() + assert not hass.states.async_all() assert "duplicate host entries found" in caplog.text From e8a8341050c5d19803e22c3c3f560a99065406df Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 14 Apr 2020 13:28:27 +0200 Subject: [PATCH 35/49] test binary sensor state --- .../components/fritzbox/test_binary_sensor.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 8f1abe13071fb9..933270cd109833 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -8,7 +8,12 @@ from homeassistant.components.binary_sensor import DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -48,11 +53,24 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state - assert state.state == "on" + assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" assert state.attributes[ATTR_DEVICE_CLASS] == "window" +async def test_is_off(hass: HomeAssistantType, fritz: Mock): + """Test state of platform.""" + device = FritzDeviceBinarySensorMock() + device.present = False + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert state.state == STATE_OFF + + async def test_update(hass: HomeAssistantType, fritz: Mock): """Test update with error.""" device = FritzDeviceBinarySensorMock() From d09df4782f0dc2a0c5e1ac99584d92e3442e5a65 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 15 Apr 2020 13:47:34 +0200 Subject: [PATCH 36/49] add config flow tests --- tests/components/fritzbox/__init__.py | 4 +- tests/components/fritzbox/test_config_flow.py | 188 ++++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 tests/components/fritzbox/test_config_flow.py diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index c79f2fc1f47f5e..42b5e05fc27217 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,11 +1,11 @@ """Tests for the AVM Fritz!Box integration.""" from unittest.mock import Mock -from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME MOCK_CONFIG = { - FB_DOMAIN: [ + DOMAIN: [ { CONF_HOST: "fake_host", CONF_PASSWORD: "fake_pass", diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py new file mode 100644 index 00000000000000..c575cacac7281f --- /dev/null +++ b/tests/components/fritzbox/test_config_flow.py @@ -0,0 +1,188 @@ +"""Tests for AVM Fritz!Box config flow.""" +from unittest.mock import patch + +from asynctest import mock +from pyfritzhome import LoginError +import pytest + +from homeassistant.components.fritzbox.const import DOMAIN +from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +from . import MOCK_CONFIG + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][0] +MOCK_SSDP_DATA = { + ATTR_SSDP_LOCATION: "https://fake_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "fake_name", +} + + +@pytest.fixture(name="fritz") +def fritz_fixture(): + """Patch libraries.""" + + with patch("homeassistant.components.fritzbox.config_flow.socket") as socket, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ) as fritz: + socket.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz + + +async def test_user(hass, fritz): + """Test starting a flow by user.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] == "fake_pass" + assert result["data"][CONF_USERNAME] == "fake_user" + + +async def test_user_auth_failed(hass, fritz): + """Test starting a flow by user with authentication failure.""" + fritz().login.side_effect = [LoginError("Boom"), mock.DEFAULT] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "auth_failed" + + +async def test_user_not_successful(hass, fritz): + """Test starting a flow by user but no connection found.""" + fritz().login.side_effect = OSError("Boom") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_found" + + +async def test_user_already_configured(hass, fritz): + """Test starting a flow by user when already configured.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import(hass, fritz): + """Test starting a flow by import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] == "fake_pass" + assert result["data"][CONF_USERNAME] == "fake_user" + + +async def test_ssdp(hass, fritz): + """Test starting a flow from discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_name" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PASSWORD] == "fake_pass" + assert result["data"][CONF_USERNAME] == "fake_user" + + +async def test_ssdp_auth_failed(hass, fritz): + """Test starting a flow from discovery with authentication failure.""" + fritz().login.side_effect = LoginError("Boom") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert result["errors"]["base"] == "auth_failed" + + +async def test_ssdp_not_successful(hass, fritz): + """Test starting a flow from discovery but no device found.""" + fritz().login.side_effect = OSError("Boom") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_found" + + +async def test_ssdp_already_in_progress(hass, fritz): + """Test starting a flow from discovery twice.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" + + +async def test_ssdp_already_configured(hass, fritz): + """Test starting a flow from discovery when already configured.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 149a1f901b31fdf37441e50f69e3a799d3afb950 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 15 Apr 2020 14:14:55 +0200 Subject: [PATCH 37/49] comments --- tests/components/fritzbox/conftest.py | 17 ++++++++++ .../components/fritzbox/test_binary_sensor.py | 20 ++--------- tests/components/fritzbox/test_climate.py | 16 +-------- tests/components/fritzbox/test_config_flow.py | 34 ++++++++----------- tests/components/fritzbox/test_init.py | 17 +--------- tests/components/fritzbox/test_sensor.py | 18 ++-------- tests/components/fritzbox/test_switch.py | 20 +++-------- 7 files changed, 42 insertions(+), 100 deletions(-) create mode 100644 tests/components/fritzbox/conftest.py diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py new file mode 100644 index 00000000000000..421aa4ea471fe6 --- /dev/null +++ b/tests/components/fritzbox/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for the AVM Fritz!Box integration.""" +from unittest.mock import Mock, patch + +import pytest + + +@pytest.fixture(name="fritz") +def fritz_fixture() -> Mock: + """Patch libraries.""" + with patch("homeassistant.components.fritzbox.socket") as socket1, patch( + "homeassistant.components.fritzbox.config_flow.socket" + ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( + "homeassistant.components.fritzbox.config_flow.Fritzhome" + ): + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield fritz diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 933270cd109833..89c1dea170440b 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,9 +1,8 @@ """Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta +from unittest import mock +from unittest.mock import Mock -from asynctest import mock -from asynctest.mock import Mock, patch -import pytest from requests.exceptions import HTTPError from homeassistant.components.binary_sensor import DOMAIN @@ -25,22 +24,9 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -@pytest.fixture(name="fritz") -def fritz_fixture(): - """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield fritz - - async def setup_fritzbox(hass: HomeAssistantType, config: dict): """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) is True + assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index f4bb29b4d95d32..c6c438963264e4 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,8 +1,7 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta +from unittest.mock import Mock -from asynctest.mock import Mock, patch -import pytest from requests.exceptions import HTTPError from homeassistant.components.climate.const import ( @@ -39,19 +38,6 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -@pytest.fixture(name="fritz") -def fritz_fixture(): - """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield fritz - - async def setup_fritzbox(hass: HomeAssistantType, config: dict): """Set up mock AVM Fritz!Box.""" assert await async_setup_component(hass, FB_DOMAIN, config) is True diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index c575cacac7281f..7ac91fccfbc285 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,13 +1,14 @@ """Tests for AVM Fritz!Box config flow.""" -from unittest.mock import patch +from unittest import mock +from unittest.mock import Mock, patch -from asynctest import mock from pyfritzhome import LoginError import pytest from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG @@ -19,9 +20,8 @@ @pytest.fixture(name="fritz") -def fritz_fixture(): +def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.config_flow.socket") as socket, patch( "homeassistant.components.fritzbox.config_flow.Fritzhome" ) as fritz: @@ -29,9 +29,8 @@ def fritz_fixture(): yield fritz -async def test_user(hass, fritz): +async def test_user(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) @@ -48,7 +47,7 @@ async def test_user(hass, fritz): assert result["data"][CONF_USERNAME] == "fake_user" -async def test_user_auth_failed(hass, fritz): +async def test_user_auth_failed(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user with authentication failure.""" fritz().login.side_effect = [LoginError("Boom"), mock.DEFAULT] @@ -60,7 +59,7 @@ async def test_user_auth_failed(hass, fritz): assert result["errors"]["base"] == "auth_failed" -async def test_user_not_successful(hass, fritz): +async def test_user_not_successful(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user but no connection found.""" fritz().login.side_effect = OSError("Boom") @@ -71,9 +70,8 @@ async def test_user_not_successful(hass, fritz): assert result["reason"] == "not_found" -async def test_user_already_configured(hass, fritz): +async def test_user_already_configured(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user when already configured.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) @@ -86,9 +84,8 @@ async def test_user_already_configured(hass, fritz): assert result["reason"] == "already_configured" -async def test_import(hass, fritz): +async def test_import(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by import.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA ) @@ -99,9 +96,8 @@ async def test_import(hass, fritz): assert result["data"][CONF_USERNAME] == "fake_user" -async def test_ssdp(hass, fritz): +async def test_ssdp(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA ) @@ -119,7 +115,7 @@ async def test_ssdp(hass, fritz): assert result["data"][CONF_USERNAME] == "fake_user" -async def test_ssdp_auth_failed(hass, fritz): +async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery with authentication failure.""" fritz().login.side_effect = LoginError("Boom") @@ -139,7 +135,7 @@ async def test_ssdp_auth_failed(hass, fritz): assert result["errors"]["base"] == "auth_failed" -async def test_ssdp_not_successful(hass, fritz): +async def test_ssdp_not_successful(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery but no device found.""" fritz().login.side_effect = OSError("Boom") @@ -157,9 +153,8 @@ async def test_ssdp_not_successful(hass, fritz): assert result["reason"] == "not_found" -async def test_ssdp_already_in_progress(hass, fritz): +async def test_ssdp_already_in_progress(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery twice.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA ) @@ -173,9 +168,8 @@ async def test_ssdp_already_in_progress(hass, fritz): assert result["reason"] == "already_in_progress" -async def test_ssdp_already_configured(hass, fritz): +async def test_ssdp_already_configured(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery when already configured.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index b44fcd5d7ce8f1..4e0b4646efdeae 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,7 +1,5 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, call, patch - -import pytest +from unittest.mock import Mock, call from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -15,19 +13,6 @@ from tests.common import MockConfigEntry -@pytest.fixture(name="fritz") -def fritz_fixture(): - """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield fritz - - async def test_setup(hass: HomeAssistantType, fritz: Mock): """Test setup of integration.""" assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 72f7939c14c4ca..6dde22f074e45d 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,8 +1,7 @@ """Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta +from unittest.mock import Mock -from asynctest.mock import Mock, patch -import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import ( @@ -27,22 +26,9 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -@pytest.fixture(name="fritz") -def fritz_fixture(): - """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield fritz - - async def setup_fritzbox(hass: HomeAssistantType, config: dict): """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) is True + assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index f231fc133e2d4e..1c0f7b3f37a9d5 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,8 +1,7 @@ """Tests for AVM Fritz!Box switch component.""" from datetime import timedelta +from unittest.mock import Mock -from asynctest.mock import Mock, patch -import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import ( @@ -21,6 +20,7 @@ ENERGY_KILO_WATT_HOUR, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_ON, TEMP_CELSIUS, ) from homeassistant.helpers.typing import HomeAssistantType @@ -34,22 +34,9 @@ ENTITY_ID = f"{DOMAIN}.fake_name" -@pytest.fixture(name="fritz") -def fritz_fixture(): - """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield fritz - - async def setup_fritzbox(hass: HomeAssistantType, config: dict): """Set up mock AVM Fritz!Box.""" - assert await async_setup_component(hass, FB_DOMAIN, config) is True + assert await async_setup_component(hass, FB_DOMAIN, config) await hass.async_block_till_done() @@ -62,6 +49,7 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state + assert state.state == STATE_ON assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678 assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" From 4e58bd86bd9d64b7389e17059ee3b6c015cb6089 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 16 Apr 2020 23:20:53 +0200 Subject: [PATCH 38/49] add missing tests --- tests/components/fritzbox/test_climate.py | 169 +++++++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index c6c438963264e4..28eed7ecabf522 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,11 +1,12 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta -from unittest.mock import Mock +from unittest.mock import Mock, call from requests.exceptions import HTTPError from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, @@ -16,6 +17,9 @@ HVAC_MODE_OFF, PRESET_COMFORT, PRESET_ECO, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, ) from homeassistant.components.fritzbox.const import ( ATTR_STATE_BATTERY_LOW, @@ -26,7 +30,12 @@ ATTR_STATE_WINDOW_OPEN, DOMAIN as FB_DOMAIN, ) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_TEMPERATURE, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -139,3 +148,159 @@ async def test_update_error(hass: HomeAssistantType, fritz: Mock): assert device.update.call_count == 1 assert fritz().login.call_count == 2 + + +async def test_set_temperature_temperature(hass: HomeAssistantType, fritz: Mock): + """Test setting temperature by temperature.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 123}, + True, + ) + assert device.set_target_temperature.call_args_list == [call(123)] + + +async def test_set_temperature_mode_off(hass: HomeAssistantType, fritz: Mock): + """Test setting temperature by mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_HVAC_MODE: HVAC_MODE_OFF, + ATTR_TEMPERATURE: 123, + }, + True, + ) + assert device.set_target_temperature.call_args_list == [call(0)] + + +async def test_set_temperature_mode_heat(hass: HomeAssistantType, fritz: Mock): + """Test setting temperature by mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_HVAC_MODE: HVAC_MODE_HEAT, + ATTR_TEMPERATURE: 123, + }, + True, + ) + assert device.set_target_temperature.call_args_list == [call(22)] + + +async def test_set_hvac_mode_off(hass: HomeAssistantType, fritz: Mock): + """Test setting hvac mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + True, + ) + assert device.set_target_temperature.call_args_list == [call(0)] + + +async def test_set_hvac_mode_heat(hass: HomeAssistantType, fritz: Mock): + """Test setting hvac mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + True, + ) + assert device.set_target_temperature.call_args_list == [call(22)] + + +async def test_set_preset_mode_comfort(hass: HomeAssistantType, fritz: Mock): + """Test setting preset mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_COMFORT}, + True, + ) + assert device.set_target_temperature.call_args_list == [call(22)] + + +async def test_set_preset_mode_eco(hass: HomeAssistantType, fritz: Mock): + """Test setting preset mode.""" + device = FritzDeviceClimateMock() + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO}, + True, + ) + assert device.set_target_temperature.call_args_list == [call(16)] + + +async def test_preset_mode_update(hass: HomeAssistantType, fritz: Mock): + """Test preset mode.""" + device = FritzDeviceClimateMock() + device.comfort_temperature = 98 + device.eco_temperature = 99 + fritz().get_devices.return_value = [device] + + await setup_fritzbox(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + + assert state + assert not state.attributes[ATTR_PRESET_MODE] + + device.target_temperature = 98 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + + assert device.update.call_count == 1 + assert state + assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT + + device.target_temperature = 99 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + + assert device.update.call_count == 2 + assert state + assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO From 7351aa72ef7438c1e46937e3813e918f4a477a97 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 16 Apr 2020 23:22:36 +0200 Subject: [PATCH 39/49] minor --- tests/components/fritzbox/test_climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 28eed7ecabf522..627eae5da91855 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -281,7 +281,7 @@ async def test_preset_mode_update(hass: HomeAssistantType, fritz: Mock): state = hass.states.get(ENTITY_ID) assert state - assert not state.attributes[ATTR_PRESET_MODE] + assert state.attributes[ATTR_PRESET_MODE] is None device.target_temperature = 98 From fb76e58532556bf138e31a16b2afec7218710e8c Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 16 Apr 2020 23:30:00 +0200 Subject: [PATCH 40/49] remove string title --- homeassistant/components/fritzbox/strings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 94b798e640b2cd..51b515d0cfac7b 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -1,6 +1,5 @@ { "config": { - "title": "AVM FRITZ!Box", "flow_title": "AVM FRITZ!Box: {name}", "step": { "user": { From ee282d37c2541070e1187f6858268234cfaaa14c Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 16 Apr 2020 23:45:15 +0200 Subject: [PATCH 41/49] deprecate yaml --- homeassistant/components/fritzbox/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 09f20d83886e1a..bd636cb8543661 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -31,6 +31,7 @@ def ensure_unique_hosts(value): CONFIG_SCHEMA = vol.Schema( + cv.deprecated(DOMAIN), { DOMAIN: vol.All( cv.ensure_list, From c77410f4a3cbf99b2b0f2224a21c67a1bcee9cc1 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 18 Apr 2020 13:45:56 +0200 Subject: [PATCH 42/49] don't change yaml --- homeassistant/components/fritzbox/__init__.py | 34 +++++++++++-------- tests/components/fritzbox/__init__.py | 18 +++++----- tests/components/fritzbox/test_config_flow.py | 4 +-- tests/components/fritzbox/test_init.py | 13 +++++-- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index bd636cb8543661..c8d4aadd20d913 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -33,20 +33,26 @@ def ensure_unique_hosts(value): CONFIG_SCHEMA = vol.Schema( cv.deprecated(DOMAIN), { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required( - CONF_USERNAME, default=DEFAULT_USERNAME - ): cv.string, - } + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required( + CONF_HOST, default=DEFAULT_HOST + ): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required( + CONF_USERNAME, default=DEFAULT_USERNAME + ): cv.string, + } + ) + ], + ensure_unique_hosts, ) - ], - ensure_unique_hosts, + } ) }, extra=vol.ALLOW_EXTRA, @@ -56,7 +62,7 @@ def ensure_unique_hosts(value): async def async_setup(hass, config): """Set up the AVM Fritz!Box integration.""" if DOMAIN in config: - for entry_config in config[DOMAIN]: + for entry_config in config[DOMAIN][CONF_DEVICES]: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=entry_config diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 42b5e05fc27217..f19e05b84dfee4 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -2,16 +2,18 @@ from unittest.mock import Mock from homeassistant.components.fritzbox.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME MOCK_CONFIG = { - DOMAIN: [ - { - CONF_HOST: "fake_host", - CONF_PASSWORD: "fake_pass", - CONF_USERNAME: "fake_user", - } - ] + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + } + ] + } } diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 7ac91fccfbc285..7327e0b539c5d9 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -7,12 +7,12 @@ from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG -MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][0] +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", ATTR_UPNP_FRIENDLY_NAME: "fake_name", diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 4e0b4646efdeae..d4c8ff9ca97205 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,7 @@ from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -30,7 +30,12 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): """Test duplicate config of integration.""" - DUPLICATE = {FB_DOMAIN: [MOCK_CONFIG[FB_DOMAIN][0], MOCK_CONFIG[FB_DOMAIN][0]]} + DUPLICATE = { + FB_DOMAIN: [ + MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + ] + } assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE) await hass.async_block_till_done() assert not hass.states.async_entity_ids() @@ -53,7 +58,9 @@ async def test_unload(hass: HomeAssistantType, fritz: Mock): entity_id = f"{SWITCH_DOMAIN}.fake_name" entry = MockConfigEntry( - domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][0], unique_id=entity_id, + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id=entity_id, ) entry.add_to_hass(hass) From 158c0cb1c9e24c447bc3c622a322cc64ae6803a2 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 18 Apr 2020 14:35:49 +0200 Subject: [PATCH 43/49] get devices async --- homeassistant/components/fritzbox/binary_sensor.py | 2 +- homeassistant/components/fritzbox/climate.py | 2 +- homeassistant/components/fritzbox/sensor.py | 2 +- homeassistant/components/fritzbox/switch.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 6f7ab25b8ed3aa..c0893b9331642f 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -13,7 +13,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - for device in fritz.get_devices(): + for device in await hass.async_add_executor_job(fritz.get_devices): if device.has_alarm and device.ain not in devices: entities.append(FritzboxBinarySensor(device, fritz)) devices.add(device.ain) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index d0e91853f59fb6..1c95d918ab86cf 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - for device in fritz.get_devices(): + for device in await hass.async_add_executor_job(fritz.get_devices): if device.has_thermostat and device.ain not in devices: entities.append(FritzboxThermostat(device, fritz)) devices.add(device.ain) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 1c6b4cc3f7daea..85238d80f27695 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - for device in fritz.get_devices(): + for device in await hass.async_add_executor_job(fritz.get_devices): if ( device.has_temperature_sensor and not device.has_switch diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index d17003b7b6d017..6f98667304b501 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] - for device in fritz.get_devices(): + for device in await hass.async_add_executor_job(fritz.get_devices): if device.has_switch and device.ain not in devices: entities.append(FritzboxSwitch(device, fritz)) devices.add(device.ain) From ec297cfbadc3910f9b721a7bc9bee58834c4fd33 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 18 Apr 2020 14:40:50 +0200 Subject: [PATCH 44/49] minor --- homeassistant/components/fritzbox/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 51b515d0cfac7b..719e534428967f 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -13,7 +13,7 @@ }, "confirm": { "title": "AVM FRITZ!Box", - "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", + "description": "Do you want to set up {name}?", "data": { "username": "Username", "password": "Password" From d94b588fbc3359a1e371805e7e6fd76e8359bcac Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 19 Apr 2020 08:28:02 +0200 Subject: [PATCH 45/49] add devices again --- tests/components/fritzbox/test_init.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index d4c8ff9ca97205..f562d7e4e8fde8 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -31,10 +31,12 @@ async def test_setup(hass: HomeAssistantType, fritz: Mock): async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog): """Test duplicate config of integration.""" DUPLICATE = { - FB_DOMAIN: [ - MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - ] + FB_DOMAIN: { + CONF_DEVICES: [ + MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + ] + } } assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE) await hass.async_block_till_done() From 7dd50274ae0c97d8fbd71c41a95c2f527982a98c Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 19 Apr 2020 13:33:20 +0200 Subject: [PATCH 46/49] comments fixed --- homeassistant/components/fritzbox/__init__.py | 29 ++++++++++--------- homeassistant/components/fritzbox/const.py | 2 +- tests/components/fritzbox/test_init.py | 9 ------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index c8d4aadd20d913..551fb9e20fce17 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,4 +1,5 @@ """Support for AVM Fritz!Box smarthome devices.""" +import asyncio import socket from pyfritzhome import Fritzhome @@ -13,13 +14,7 @@ ) import homeassistant.helpers.config_validation as cv -from .const import ( - CONF_CONNECTIONS, - DEFAULT_HOST, - DEFAULT_USERNAME, - DOMAIN, - SUPPORTED_DOMAINS, -) +from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS def ensure_unique_hosts(value): @@ -30,7 +25,7 @@ def ensure_unique_hosts(value): return value -CONFIG_SCHEMA = vol.Schema( +CONFIG_SCHEMA = vol.All( cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( @@ -84,9 +79,9 @@ async def async_setup_entry(hass, entry): hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz - for domain in SUPPORTED_DOMAINS: + for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, domain) + hass.config_entries.async_forward_entry_setup(entry, component) ) def logout_fritzbox(event): @@ -103,7 +98,15 @@ async def async_unload_entry(hass, entry): fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] await hass.async_add_executor_job(fritz.logout) - for domain in SUPPORTED_DOMAINS: - await hass.config_entries.async_forward_entry_unload(entry, domain) + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][CONF_CONNECTIONS].pop(entry.entry_id) - return True + return unload_ok diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 53a53a78ab0022..32a72e8e7a6a3e 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -22,4 +22,4 @@ LOGGER = logging.getLogger(__package__) -SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"] +PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index f562d7e4e8fde8..11067c1aa51bf5 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -45,15 +45,6 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl assert "duplicate host entries found" in caplog.text -async def test_setup_duplicate_entries(hass: HomeAssistantType, fritz: Mock): - """Test duplicate setup of integration.""" - assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) is True - await hass.async_block_till_done() - assert len(hass.config_entries.async_entries()) == 1 - assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG) is True - assert len(hass.config_entries.async_entries()) == 1 - - async def test_unload(hass: HomeAssistantType, fritz: Mock): """Test unload of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] From ac7a8dc0c12a101a08213a79279085fb3c005cf8 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 19 Apr 2020 14:02:40 +0200 Subject: [PATCH 47/49] unique_id fixes --- homeassistant/components/fritzbox/__init__.py | 54 ++++++++++--------- .../components/fritzbox/config_flow.py | 26 +++++---- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 551fb9e20fce17..53e0d555b6efe9 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -25,32 +25,34 @@ def ensure_unique_hosts(value): return value -CONFIG_SCHEMA = vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICES): vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required( - CONF_HOST, default=DEFAULT_HOST - ): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required( - CONF_USERNAME, default=DEFAULT_USERNAME - ): cv.string, - } - ) - ], - ensure_unique_hosts, - ) - } - ) - }, - extra=vol.ALLOW_EXTRA, +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_DEVICES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required( + CONF_HOST, default=DEFAULT_HOST + ): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required( + CONF_USERNAME, default=DEFAULT_USERNAME + ): cv.string, + } + ) + ], + ensure_unique_hosts, + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, + ) ) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 5d8bcccd88559f..1b086f5815979a 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -1,5 +1,4 @@ """Config flow for AVM Fritz!Box.""" -import socket from urllib.parse import urlparse from pyfritzhome import Fritzhome, LoginError @@ -82,12 +81,14 @@ async def async_step_user(self, user_input=None): errors = {} if user_input is not None: - ip_address = await self.hass.async_add_executor_job( - socket.gethostbyname, user_input[CONF_HOST] - ) - await self.async_set_unique_id(ip_address, raise_on_progress=False) - self._abort_if_unique_id_configured() + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + if entry.data != user_input: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + return self.async_abort(reason="already_configured") self._host = user_input[CONF_HOST] self._name = user_input[CONF_HOST] @@ -109,10 +110,17 @@ async def async_step_user(self, user_input=None): async def async_step_ssdp(self, user_input): """Handle a flow initialized by discovery.""" host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname - ip_address = await self.hass.async_add_executor_job(socket.gethostbyname, host) + self.context[CONF_HOST] = host - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured() + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + return self.async_abort(reason="already_in_progress") + + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == host: + if entry.data != user_input: + self.hass.config_entries.async_update_entry(entry, data=user_input) + return self.async_abort(reason="already_configured") self._host = host self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] From 5a797a0b5da358704790c9b11d865f1cafd0ba77 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 19 Apr 2020 20:39:31 +0200 Subject: [PATCH 48/49] fix patches --- tests/components/fritzbox/conftest.py | 11 ++++------- tests/components/fritzbox/test_config_flow.py | 5 +---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 421aa4ea471fe6..591c10375256d4 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -7,11 +7,8 @@ @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.socket") as socket1, patch( - "homeassistant.components.fritzbox.config_flow.socket" - ) as socket2, patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ): - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + with patch("homeassistant.components.fritzbox.socket") as socket, patch( + "homeassistant.components.fritzbox.Fritzhome" + ) as fritz, patch("homeassistant.components.fritzbox.config_flow.Fritzhome"): + socket.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield fritz diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 7327e0b539c5d9..d6b43dc4b71adb 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -22,10 +22,7 @@ @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.config_flow.socket") as socket, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ) as fritz: - socket.gethostbyname.return_value = "FAKE_IP_ADDRESS" + with patch("homeassistant.components.fritzbox.config_flow.Fritzhome") as fritz: yield fritz From 4b848ff5e4d1a8eac2667c1762ca16382d363e45 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 20 Apr 2020 02:24:44 +0200 Subject: [PATCH 49/49] Fix schema --- homeassistant/components/fritzbox/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 53e0d555b6efe9..7297f514f9665c 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -51,8 +51,8 @@ def ensure_unique_hosts(value): } ) }, - extra=vol.ALLOW_EXTRA, - ) + ), + extra=vol.ALLOW_EXTRA, )