From 207cf18a46a30d073193ec1500cb96d6b8a28f7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Jul 2019 16:19:46 -0700 Subject: [PATCH 001/213] Bump version to 0.97.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17e0e6752d2447..54f4645edaa332 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 59b42b4236b0ccb7a787564ab924f30ab8e7f0b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 12:32:48 -0700 Subject: [PATCH 002/213] Expose comfort presets as HA presets (#25491) * Expose comfort presets as HA presets * Fix bugs * Handle unavailable * log level debug on update * Lint --- homeassistant/components/ecobee/__init__.py | 2 +- homeassistant/components/ecobee/climate.py | 65 +++++++++------------ tests/components/ecobee/test_climate.py | 14 ----- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index e69884af59f52c..cb8b7436b51089 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -97,7 +97,7 @@ def __init__(self, config_file): def update(self): """Get the latest data from pyecobee.""" self.ecobee.update() - _LOGGER.info("Ecobee data updated successfully") + _LOGGER.debug("Ecobee data updated successfully") def setup(hass, config): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6520a3aadba16a..d9af0f93e115ea 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -88,16 +88,6 @@ PRESET_HOLD_INDEFINITE: "indefinite", } -PRESET_MODES = [ - PRESET_NONE, - PRESET_AWAY, - PRESET_TEMPERATURE, - PRESET_HOME, - PRESET_SLEEP, - PRESET_HOLD_NEXT_TRANSITION, - PRESET_HOLD_INDEFINITE, -] - SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" SERVICE_RESUME_PROGRAM = "ecobee_resume_program" @@ -199,7 +189,6 @@ def __init__(self, data, thermostat_index, hold_temp): self._name = self.thermostat["name"] self.hold_temp = hold_temp self.vacation = None - self._climate_list = self.climate_list self._operation_list = [] if self.thermostat["settings"]["heatStages"]: @@ -210,6 +199,10 @@ def __init__(self, data, thermostat_index, hold_temp): self._operation_list.insert(0, HVAC_MODE_AUTO) self._operation_list.append(HVAC_MODE_OFF) + self._preset_modes = { + comfort["climateRef"]: comfort["name"] + for comfort in self.thermostat["program"]["climates"] + } self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False @@ -223,6 +216,11 @@ def update(self): self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + @property + def available(self): + """Return if device is available.""" + return self.thermostat["runtime"]["connected"] + @property def supported_features(self): """Return the list of supported features.""" @@ -294,15 +292,9 @@ def preset_mode(self): continue if event["type"] == "hold": - if event["holdClimateRef"] == "away": - if int(event["endDate"][0:4]) - int(event["startDate"][0:4]) <= 1: - # A temporary hold from away climate is a hold - return PRESET_AWAY - # A permanent hold from away climate - return PRESET_AWAY - if event["holdClimateRef"] != "": - # Any other hold based on climate - return event["holdClimateRef"] + if event["holdClimateRef"] in self._preset_modes: + return self._preset_modes[event["holdClimateRef"]] + # Any hold not based on a climate is a temp hold return PRESET_TEMPERATURE if event["type"].startswith("auto"): @@ -324,14 +316,6 @@ def hvac_modes(self): """Return the operation modes list.""" return self._operation_list - @property - def climate_mode(self): - """Return current mode, as the user-visible name.""" - cur = self.thermostat["program"]["currentClimateRef"] - climates = self.thermostat["program"]["climates"] - current = list(filter(lambda x: x["climateRef"] == cur, climates)) - return current[0]["name"] - @property def current_humidity(self) -> Optional[int]: """Return the current humidity.""" @@ -373,9 +357,7 @@ def device_state_attributes(self): status = self.thermostat["equipmentStatus"] return { "fan": self.fan, - "climate_mode": self.climate_mode, "equipment_running": status, - "climate_list": self.climate_list, "fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"], } @@ -413,6 +395,21 @@ def set_preset_mode(self, preset_mode): elif preset_mode == PRESET_NONE: self.data.ecobee.resume_program(self.thermostat_index) + elif preset_mode in self.preset_modes: + climate_ref = None + + for comfort in self.thermostat["program"]["climates"]: + if comfort["name"] == preset_mode: + climate_ref = comfort["climateRef"] + break + + if climate_ref is not None: + self.data.ecobee.set_climate_hold( + self.thermostat_index, climate_ref, self.hold_preference() + ) + else: + _LOGGER.warning("Received unknown preset mode: %s", preset_mode) + else: self.data.ecobee.set_climate_hold( self.thermostat_index, preset_mode, self.hold_preference() @@ -421,7 +418,7 @@ def set_preset_mode(self, preset_mode): @property def preset_modes(self): """Return available preset modes.""" - return PRESET_MODES + return list(self._preset_modes.values()) def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" @@ -543,9 +540,3 @@ def hold_preference(self): # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode return "nextTransition" - - @property - def climate_list(self): - """Return the list of climates currently available.""" - climates = self.thermostat["program"]["climates"] - return list(map((lambda x: x["name"]), climates)) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index fa3f84b4b122b6..24938e526213cc 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -130,44 +130,34 @@ def test_device_state_attributes(self): """Test device state attributes property.""" self.ecobee["equipmentStatus"] = "heatPump2" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "heatPump2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "auxHeat2" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "auxHeat2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "compCool1" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "compCool1", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "Unknown" assert { - "climate_list": ["Climate1", "Climate2"], "fan": "off", "fan_min_on_time": 10, - "climate_mode": "Climate1", "equipment_running": "Unknown", } == self.thermostat.device_state_attributes @@ -267,10 +257,6 @@ def test_hold_preference(self): self.ecobee["settings"]["holdAction"] = action assert "nextTransition" == self.thermostat.hold_preference() - def test_climate_list(self): - """Test climate list property.""" - assert ["Climate1", "Climate2"] == self.thermostat.climate_list - def test_set_fan_mode_on(self): """Test set fan mode to on.""" self.data.reset_mock() From d3f6c43bbdcf920661d4538470647a2ddb070999 Mon Sep 17 00:00:00 2001 From: Martin Eberhardt Date: Thu, 1 Aug 2019 22:02:11 +0200 Subject: [PATCH 003/213] Fix handling of empty results from Rejseplanen (#25610) * Improve handling of empty results from Rejseplanen (Fixes #25566) * Exclude attributes with null value * Add period back into docstring * Fix formatting --- .../components/rejseplanen/sensor.py | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 3ba2b06eb02f6b..99cfe1067e8932 100755 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -111,14 +111,14 @@ def state(self): def device_state_attributes(self): """Return the state attributes.""" if not self._times: - return None + return {ATTR_STOP_ID: self._stop_id, ATTR_ATTRIBUTION: ATTRIBUTION} next_up = [] if len(self._times) > 1: next_up = self._times[1:] - params = { - ATTR_DUE_IN: str(self._times[0][ATTR_DUE_IN]), + return { + ATTR_DUE_IN: self._times[0][ATTR_DUE_IN], ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], ATTR_TYPE: self._times[0][ATTR_TYPE], ATTR_ROUTE: self._times[0][ATTR_ROUTE], @@ -128,7 +128,6 @@ def device_state_attributes(self): ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up, } - return {k: v for k, v in params.items() if v} @property def unit_of_measurement(self): @@ -144,10 +143,14 @@ def update(self): """Get the latest data from rejseplanen.dk and update the states.""" self.data.update() self._times = self.data.info - try: - self._state = self._times[0][ATTR_DUE_IN] - except TypeError: - pass + + if not self._times: + self._state = None + else: + try: + self._state = self._times[0][ATTR_DUE_IN] + except TypeError: + pass class PublicTransportData: @@ -159,20 +162,7 @@ def __init__(self, stop_id, route, direction, departure_type): self.route = route self.direction = direction self.departure_type = departure_type - self.info = self.empty_result() - - def empty_result(self): - """Object returned when no departures are found.""" - return [ - { - ATTR_DUE_IN: "n/a", - ATTR_DUE_AT: "n/a", - ATTR_TYPE: "n/a", - ATTR_ROUTE: self.route, - ATTR_DIRECTION: "n/a", - ATTR_STOP_NAME: "n/a", - } - ] + self.info = [] def update(self): """Get the latest data from rejseplanen.""" @@ -200,11 +190,9 @@ def intersection(lst1, lst2): ) except rjpl.rjplAPIError as error: _LOGGER.debug("API returned error: %s", error) - self.info = self.empty_result() return except (rjpl.rjplConnectionError, rjpl.rjplHTTPError): _LOGGER.debug("Error occured while connecting to the API") - self.info = self.empty_result() return # Filter result @@ -246,7 +234,6 @@ def intersection(lst1, lst2): if not self.info: _LOGGER.debug("No departures with given parameters") - self.info = self.empty_result() # Sort the data by time self.info = sorted(self.info, key=itemgetter(ATTR_DUE_IN)) From 7168dd6ceca3e7bf1fce2e69052d661ee479f16b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 31 Jul 2019 23:33:12 -0400 Subject: [PATCH 004/213] bump quirks (#25618) --- homeassistant/components/zha/manifest.json | 7 ++----- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 199428302add41..88c5f171116b39 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,14 +5,11 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.9.0", - "zha-quirks==0.0.19", + "zha-quirks==0.0.20", "zigpy-deconz==0.2.1", "zigpy-homeassistant==0.7.0", "zigpy-xbee-homeassistant==0.4.0" ], "dependencies": [], - "codeowners": [ - "@dmulcahey", - "@adminiuga" - ] + "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf06f2fbe6526a..c6e0ca6abf1145 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1963,7 +1963,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.19 +zha-quirks==0.0.20 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 56ca0edaa7fbc46a7c2d2f29277c53af481a7176 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Aug 2019 17:22:08 +0200 Subject: [PATCH 005/213] Handle disabled devices (#25625) --- homeassistant/components/unifi/device_tracker.py | 2 +- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_device_tracker.py | 10 ++++++++++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index e7344692ecd227..4046f5f63d2235 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -304,7 +304,7 @@ def unique_id(self) -> str: @property def available(self) -> bool: """Return if controller is available.""" - return self.controller.available + return not self.device.disabled and self.controller.available @property def device_info(self): diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index dc5e89c147ed24..e849fd34d2582f 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==8" + "aiounifi==9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6e0ca6abf1145..5d365c9c732dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==8 +aiounifi==9 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2e4cfef22de22..1feeab8f32d03c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==8 +aiounifi==9 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 937de3ad631dfb..5accbb584b48a0 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -203,6 +203,16 @@ async def test_tracked_devices(hass, mock_controller): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" + device_1_copy = copy(DEVICE_1) + device_1_copy["disabled"] = True + mock_controller.mock_client_responses.append({}) + mock_controller.mock_device_responses.append([device_1_copy]) + await mock_controller.async_update() + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "unavailable" + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" From 414b85c253f86fba469cd412f7eadd509fe975b2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Aug 2019 16:35:19 +0100 Subject: [PATCH 006/213] Fix polling HomeKit devices with multiple services per accessory (#25629) --- homeassistant/components/homekit_controller/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index fd9c960980cd4b..79636cea9f3918 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -120,13 +120,21 @@ def async_state_changed(self): """Collect new data from bridge and update the entity state in hass.""" accessory_state = self._accessory.current_state.get(self._aid, {}) for iid, result in accessory_state.items(): + # No value so dont process this result if "value" not in result: continue + + # Unknown iid - this is probably for a sibling service that is part + # of the same physical accessory. Ignore it. + if iid not in self._char_names: + continue + # Callback to update the entity with this characteristic value char_name = escape_characteristic_name(self._char_names[iid]) update_fn = getattr(self, "_update_{}".format(char_name), None) if not update_fn: continue + # pylint: disable=not-callable update_fn(result["value"]) From 725d5c636e12feff5fc73bf014058193b9dcc771 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Thu, 1 Aug 2019 21:45:16 +0200 Subject: [PATCH 007/213] Meteofrance improve log error messages (#25630) * Improve log error messages * remove unique_id not ready yet --- homeassistant/components/meteo_france/__init__.py | 14 +++++++++++--- homeassistant/components/meteo_france/sensor.py | 12 ++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index d227a7fe47cc9e..ab3ec45867b204 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -113,13 +113,17 @@ def setup(hass, config): # If weather alert monitoring is expected initiate a client to be used by # all weather_alert entities. if need_weather_alert_watcher: + _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") from vigilancemeteo import VigilanceMeteoFranceProxy, VigilanceMeteoError weather_alert_client = VigilanceMeteoFranceProxy() try: weather_alert_client.update_data() except VigilanceMeteoError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the" "vigilance_meteoFrance proxy: %s ", + exp, + ) else: weather_alert_client = None hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client @@ -133,7 +137,9 @@ def setup(hass, config): try: client = meteofranceClient(city) except meteofranceError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the meteofrance proxy: %s", exp + ) return client.need_rain_forecast = bool( @@ -179,4 +185,6 @@ def update(self): try: self._client.update() except meteofranceError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when updating the meteofrance proxy: %s", exp + ) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 9ee9ce9cef6bb7..95113a60cd38ee 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -35,18 +35,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): datas["dept"], weather_alert_client ) except ValueError as exp: - _LOGGER.error(exp) + _LOGGER.error( + "Unexpected error when creating the weather alert sensor for %s in department %s: %s", + city, + datas["dept"], + exp, + ) alert_watcher = None else: _LOGGER.info( - "weather alert watcher added for %s" "in department %s", + "Weather alert watcher added for %s" "in department %s", city, datas["dept"], ) else: _LOGGER.warning( - "No dept key found for '%s'. So weather alert " - "information won't be available", + "No 'dept' key found for '%s'. So weather alert information won't be available", city, ) # Exit and don't create the sensor if no department code available. From 1d5709f49f6c040173a763c94353f4f0f277b5d1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Aug 2019 20:44:30 +0100 Subject: [PATCH 008/213] Bump homekit_python to 0.15 (#25631) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 62dbf3740a3f7a..70f6f6a3ce40cf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homekit_controller", "requirements": [ - "homekit[IP]==0.14.0" + "homekit[IP]==0.15.0" ], "dependencies": [], "zeroconf": ["_hap._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 5d365c9c732dee..b390f7ee5313c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ home-assistant-frontend==20190731.0 homeassistant-pyozw==0.1.4 # homeassistant.components.homekit_controller -homekit[IP]==0.14.0 +homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud homematicip==0.10.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1feeab8f32d03c..2bedea3d2d64c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -172,7 +172,7 @@ holidays==0.9.11 home-assistant-frontend==20190731.0 # homeassistant.components.homekit_controller -homekit[IP]==0.14.0 +homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud homematicip==0.10.9 From 476a727df3bcc6e553449ecb238d95f3f37244a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 11:52:57 -0700 Subject: [PATCH 009/213] Filter out empty results in history API (#25633) --- homeassistant/components/history/__init__.py | 6 ++++-- tests/components/history/test_init.py | 22 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 3b751b86c73693..d402aceaa402b7 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -234,6 +234,7 @@ def states_to_json( axis correctly. """ result = defaultdict(list) + # Set all entity IDs to empty lists in result set to maintain the order if entity_ids is not None: for ent_id in entity_ids: result[ent_id] = [] @@ -253,7 +254,9 @@ def states_to_json( # Append all changes to it for ent_id, group in groupby(states, lambda state: state.entity_id): result[ent_id].extend(group) - return result + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} def get_state(hass, utc_point_in_time, entity_id, run=None): @@ -348,7 +351,6 @@ async def get(self, request, datetime=None): # Optionally reorder the result to respect the ordering given # by any entities explicitly included in the configuration. - if self.use_include_order: sorted_result = [] for order_entity in self.filters.included_entities: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index ebd5991235dfbf..68bc9c5371fc4e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -628,3 +628,25 @@ async def test_fetch_period_api(hass, hass_client): "/api/history/period/{}".format(dt_util.utcnow().isoformat()) ) assert response.status == 200 + + +async def test_fetch_period_api_with_include_order(hass, hass_client): + """Test the fetch period view for history.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + { + "history": { + "use_include_order": True, + "include": {"entities": ["light.kitchen"]}, + } + }, + ) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await hass_client() + response = await client.get( + "/api/history/period/{}".format(dt_util.utcnow().isoformat()), + params={"filter_entity_id": "non.existing,something.else"}, + ) + assert response.status == 200 From a8c4fc33f60291ca17e6e61cf512591203081e6c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 13:03:45 -0700 Subject: [PATCH 010/213] Upgrade hass-nabucasa to 0.16 (#25636) --- homeassistant/components/cloud/manifest.json | 13 +++---------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e848f54425b3d4..58739bededc7b3 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,14 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": [ - "hass-nabucasa==0.15" - ], - "dependencies": [ - "http", - "webhook" - ], - "codeowners": [ - "@home-assistant/cloud" - ] + "requirements": ["hass-nabucasa==0.16"], + "dependencies": ["http", "webhook"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 187ee921e2552c..c5c8dd9c9c008d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ bcrypt==3.1.7 certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.15 +hass-nabucasa==0.16 home-assistant-frontend==20190731.0 importlib-metadata==0.18 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index b390f7ee5313c9..7b88be5c023f66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -592,7 +592,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.15 +hass-nabucasa==0.16 # homeassistant.components.mqtt hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bedea3d2d64c1..33815c4c0dd9fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -157,7 +157,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.15 +hass-nabucasa==0.16 # homeassistant.components.mqtt hbmqtt==0.9.4 From d64730a3cfe4e6e34821680af5c421854174783c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 13:34:28 -0700 Subject: [PATCH 011/213] Updated frontend to 20190801.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c3b7fa3e392cb4..b6a996afc98d63 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190731.0" + "home-assistant-frontend==20190801.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5c8dd9c9c008d..942cdb577f95c7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7b88be5c023f66..62de816853c273 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33815c4c0dd9fe..2d3e89dc27050a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190731.0 +home-assistant-frontend==20190801.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From ad341e21529fbe4615c1423ac602562156684f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 13:36:24 -0700 Subject: [PATCH 012/213] Bumped version to 0.97.0b1 --- homeassistant/const.py | 2 +- script/version_bump.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54f4645edaa332..4de4048d4f928c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) diff --git a/script/version_bump.py b/script/version_bump.py index 7c584daae7ea6f..db3f3ac273dbd5 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -102,7 +102,7 @@ def write_version(version): "MINOR_VERSION = .*\n", "MINOR_VERSION = {}\n".format(minor), content ) content = re.sub( - "PATCH_VERSION = .*\n", "PATCH_VERSION = '{}'\n".format(patch), content + "PATCH_VERSION = .*\n", 'PATCH_VERSION = "{}"\n'.format(patch), content ) with open("homeassistant/const.py", "wt") as fil: From 949875ae501984e75b4ea66a440f00abb365c6a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Aug 2019 22:30:31 -0700 Subject: [PATCH 013/213] Updated frontend to 20190804.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b6a996afc98d63..4cea624b4cd072 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190801.0" + "home-assistant-frontend==20190804.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 942cdb577f95c7..13c5b1a01443b8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62de816853c273..d70c0602c4c2d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d3e89dc27050a..0d71d23b74ff7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190801.0 +home-assistant-frontend==20190804.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 8d1deef9cc891e8dc2a896faaf26b21409b26493 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 2 Aug 2019 10:00:33 +0200 Subject: [PATCH 014/213] Feature zwave preset modes (#25537) * Initial commit * Add some more code * Local tests passing * Remove unnecessary line * Add preset attributes to __init__ * Remove some more debugger lines * Add some tests * Fix comparision to None * Improve test coverage * Use unknown modes as presets * Bugfixes and test improvements * Add tests for unknown preset modes * linting * Improve mappings * Move PRESET_MANUFACTURER_SPECIFIC to zwave * Replace isinstance with cast * Add test for hvac_action * hvac_mode is never None * Improved mapping of current mode to hvac/preset modes * Fix bugs where hvac_mode is None * Add default hvac mode * Fixed default hvac mode * Fix linting * Make flake happy * Another linting * Make black happy * Complete list of default hvac modes * Add mapping to heat/cool eco * Fixed another bug where mapping goes wrong --- homeassistant/components/zwave/climate.py | 203 ++++++++++++--- tests/components/zwave/test_climate.py | 292 ++++++++++++++++++++-- 2 files changed, 439 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 15a0c5ab78bae3..6f66c6f36c460b 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -10,15 +10,19 @@ CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, DOMAIN, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_NONE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_PRESET_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback @@ -37,38 +41,57 @@ ATTR_OPERATING_STATE = "operating_state" ATTR_FAN_STATE = "fan_state" + +# Device is in manufacturer specific mode (e.g. setting the valve manually) +PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" + WORKAROUND_ZXT_120 = "zxt_120" DEVICE_MAPPINGS = {REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120} HVAC_STATE_MAPPINGS = { - "Off": HVAC_MODE_OFF, - "Heat": HVAC_MODE_HEAT, - "Heat Mode": HVAC_MODE_HEAT, - "Heat (Default)": HVAC_MODE_HEAT, - "Aux Heat": HVAC_MODE_HEAT, - "Furnace": HVAC_MODE_HEAT, - "Fan Only": HVAC_MODE_FAN_ONLY, - "Dry Air": HVAC_MODE_DRY, - "Moist Air": HVAC_MODE_DRY, - "Cool": HVAC_MODE_COOL, - "Auto": HVAC_MODE_HEAT_COOL, + "off": HVAC_MODE_OFF, + "heat": HVAC_MODE_HEAT, + "heat mode": HVAC_MODE_HEAT, + "heat (default)": HVAC_MODE_HEAT, + "aux heat": HVAC_MODE_HEAT, + "furnace": HVAC_MODE_HEAT, + "fan only": HVAC_MODE_FAN_ONLY, + "dry air": HVAC_MODE_DRY, + "moist air": HVAC_MODE_DRY, + "cool": HVAC_MODE_COOL, + "heat_cool": HVAC_MODE_HEAT_COOL, + "auto": HVAC_MODE_HEAT_COOL, } - HVAC_CURRENT_MAPPINGS = { - "Idle": CURRENT_HVAC_IDLE, - "Heat": CURRENT_HVAC_HEAT, - "Pending Heat": CURRENT_HVAC_IDLE, - "Heating": CURRENT_HVAC_HEAT, - "Cool": CURRENT_HVAC_COOL, - "Pending Cool": CURRENT_HVAC_IDLE, - "Cooling": CURRENT_HVAC_COOL, - "Fan Only": CURRENT_HVAC_FAN, - "Vent / Economiser": CURRENT_HVAC_FAN, - "Off": CURRENT_HVAC_OFF, + "idle": CURRENT_HVAC_IDLE, + "heat": CURRENT_HVAC_HEAT, + "pending heat": CURRENT_HVAC_IDLE, + "heating": CURRENT_HVAC_HEAT, + "cool": CURRENT_HVAC_COOL, + "pending cool": CURRENT_HVAC_IDLE, + "cooling": CURRENT_HVAC_COOL, + "fan only": CURRENT_HVAC_FAN, + "vent / economiser": CURRENT_HVAC_FAN, + "off": CURRENT_HVAC_OFF, } +PRESET_MAPPINGS = { + "full power": PRESET_BOOST, + "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, +} + +DEFAULT_HVAC_MODES = [ + HVAC_MODE_HEAT_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, +] + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave climate devices.""" @@ -101,9 +124,13 @@ def __init__(self, values, temp_unit): self._target_temperature = None self._current_temperature = None self._hvac_action = None - self._hvac_list = None - self._hvac_mapping = None - self._hvac_mode = None + self._hvac_list = None # [zwave_mode] + self._hvac_mapping = None # {ha_mode:zwave_mode} + self._hvac_mode = None # ha_mode + self._default_hvac_mode = None # ha_mode + self._preset_mapping = None # {ha_mode:zwave_mode} + self._preset_list = None # [zwave_mode] + self._preset_mode = None # ha_mode if exists, else zwave_mode self._current_fan_mode = None self._fan_modes = None self._fan_state = None @@ -132,6 +159,8 @@ def supported_features(self): support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: support |= SUPPORT_SWING_MODE + if self._preset_list: + support |= SUPPORT_PRESET_MODE return support def update_properties(self): @@ -140,26 +169,86 @@ def update_properties(self): if self.values.mode: self._hvac_list = [] self._hvac_mapping = {} - hvac_list = self.values.mode.data_items - if hvac_list: - for mode in hvac_list: - ha_mode = HVAC_STATE_MAPPINGS.get(mode) + self._preset_list = [] + self._preset_mapping = {} + + mode_list = self.values.mode.data_items + if mode_list: + for mode in mode_list: + ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) + ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) if ha_mode and ha_mode not in self._hvac_mapping: self._hvac_mapping[ha_mode] = mode self._hvac_list.append(ha_mode) - continue - self._hvac_list.append(mode) + elif ha_preset and ha_preset not in self._preset_mapping: + self._preset_mapping[ha_preset] = mode + self._preset_list.append(ha_preset) + else: + # If nothing matches + self._preset_list.append(mode) + + # Default operation mode + for mode in DEFAULT_HVAC_MODES: + if mode in self._hvac_mapping.keys(): + self._default_hvac_mode = mode + break + + if self._preset_list: + # Presets are supported + self._preset_list.append(PRESET_NONE) + current_mode = self.values.mode.data - self._hvac_mode = next( + _LOGGER.debug("current_mode=%s", current_mode) + _hvac_temp = next( ( key for key, value in self._hvac_mapping.items() if value == current_mode ), - current_mode, + None, ) + + if _hvac_temp is None: + # The current mode is not a hvac mode + if ( + "heat" in current_mode.lower() + and HVAC_MODE_HEAT in self._hvac_mapping.keys() + ): + # The current preset modes maps to HVAC_MODE_HEAT + _LOGGER.debug("Mapped to HEAT") + self._hvac_mode = HVAC_MODE_HEAT + elif ( + "cool" in current_mode.lower() + and HVAC_MODE_COOL in self._hvac_mapping.keys() + ): + # The current preset modes maps to HVAC_MODE_COOL + _LOGGER.debug("Mapped to COOL") + self._hvac_mode = HVAC_MODE_COOL + else: + # The current preset modes maps to self._default_hvac_mode + _LOGGER.debug("Mapped to DEFAULT") + self._hvac_mode = self._default_hvac_mode + self._preset_mode = next( + ( + key + for key, value in self._preset_mapping.items() + if value == current_mode + ), + current_mode, + ) + else: + # The current mode is a hvac mode + self._hvac_mode = _hvac_temp + self._preset_mode = PRESET_NONE + + _LOGGER.debug("self._hvac_mapping=%s", self._hvac_mapping) _LOGGER.debug("self._hvac_list=%s", self._hvac_list) + _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) + _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) _LOGGER.debug("self._hvac_action=%s", self._hvac_action) + _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) + _LOGGER.debug("self._preset_list=%s", self._preset_list) + _LOGGER.debug("self._preset_mode=%s", self._preset_mode) # Current Temp if self.values.temperature: @@ -199,7 +288,7 @@ def update_properties(self): # Operating state if self.values.operating_state: mode = self.values.operating_state.data - self._hvac_action = HVAC_CURRENT_MAPPINGS.get(mode) + self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) # Fan operating state if self.values.fan_state: @@ -247,7 +336,7 @@ def hvac_mode(self): """ if self.values.mode: return self._hvac_mode - return HVAC_MODE_HEAT + return self._default_hvac_mode @property def hvac_modes(self): @@ -267,6 +356,26 @@ def hvac_action(self): """ return self._hvac_action + @property + def preset_mode(self): + """Return preset operation ie. eco, away. + + Need to be one of PRESET_*. + """ + if self.values.mode: + return self._preset_mode + return PRESET_NONE + + @property + def preset_modes(self): + """Return the list of available preset operation modes. + + Need to be a subset of PRESET_MODES. + """ + if self.values.mode: + return self._preset_list + return [] + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -274,24 +383,46 @@ def target_temperature(self): def set_temperature(self, **kwargs): """Set new target temperature.""" + _LOGGER.debug("Set temperature to %s", kwargs.get(ATTR_TEMPERATURE)) if kwargs.get(ATTR_TEMPERATURE) is None: return self.values.primary.data = kwargs.get(ATTR_TEMPERATURE) def set_fan_mode(self, fan_mode): """Set new target fan mode.""" + _LOGGER.debug("Set fan mode to %s", fan_mode) if not self.values.fan_mode: return self.values.fan_mode.data = fan_mode def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + _LOGGER.debug("Set hvac_mode to %s", hvac_mode) if not self.values.mode: return - self.values.mode.data = self._hvac_mapping.get(hvac_mode, hvac_mode) + operation_mode = self._hvac_mapping.get(hvac_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode + + def set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + _LOGGER.debug("Set preset_mode to %s", preset_mode) + if not self.values.mode: + return + if preset_mode == PRESET_NONE: + # Activate the current hvac mode + self.update_properties() + operation_mode = self._hvac_mapping.get(self.hvac_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode + else: + operation_mode = self._preset_mapping.get(preset_mode, preset_mode) + _LOGGER.debug("Set operation_mode to %s", operation_mode) + self.values.mode.data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" + _LOGGER.debug("Set swing_mode to %s", swing_mode) if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self.values.zxt_120_swing_mode.data = swing_mode diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 430b901efbc90f..60a9dcd0dabca3 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -2,11 +2,23 @@ import pytest from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + HVAC_MODES, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.zwave import climate +from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -19,9 +31,18 @@ def device(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="test1", data_items=[0, 1, 2], node=node), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_state=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -37,9 +58,18 @@ def device_zxt_120(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="test1", data_items=[0, 1, 2], node=node), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_state=MockValue(data=7, node=node), zxt_120_swing_mode=MockValue(data="test3", data_items=[6, 7, 8], node=node), ) @@ -55,9 +85,13 @@ def device_mapping(hass, mock_openzwave): values = MockEntityValues( primary=MockValue(data=1, node=node), temperature=MockValue(data=5, node=node, units=None), - mode=MockValue(data="Off", data_items=["Off", "Cool", "Heat"], node=node), + mode=MockValue( + data="Heat", + data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + node=node, + ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=6, node=node), + operating_state=MockValue(data="heating", node=node), fan_state=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -65,6 +99,83 @@ def device_mapping(hass, mock_openzwave): yield device +@pytest.fixture +def device_unknown(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state unknown.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue( + data="Heat", + data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], + node=node, + ), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_state=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state heat only.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue( + data=HVAC_MODE_HEAT, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + "Heat Eco", + "Cool Eco", + ], + node=node, + ), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_state=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_default_hvac_modes(): + """Test wether all hvac modes are included in default_hvac_modes.""" + for hvac_mode in HVAC_MODES: + assert hvac_mode in DEFAULT_HVAC_MODES + + +def test_supported_features(device): + """Test supported features flags.""" + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_preset_mode(device_mapping): + """Test supported features flags with swing mode.""" + device = device_mapping + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + ) + + +def test_supported_features_swing_mode(device_zxt_120): + """Test supported features flags with swing mode.""" + device = device_zxt_120 + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + ) + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -107,7 +218,24 @@ def test_default_target_temperature(device): def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] - assert device.hvac_modes == [0, 1, 2] + assert device.hvac_modes == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ] + assert device.preset_modes == [] + device.values.mode = None + assert device.preset_modes == [] + + +def test_data_lists_mapping(device_mapping): + """Test data lists from zwave value items.""" + device = device_mapping + assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] + assert device.preset_modes == ["boost", "none"] + device.values.mode = None + assert device.preset_modes == [] def test_target_value_set(device): @@ -121,21 +249,56 @@ def test_target_value_set(device): def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == "test1" - device.set_hvac_mode("test_set") - assert device.values.mode.data == "test_set" + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_hvac_mode(HVAC_MODE_COOL) + assert device.values.mode.data == HVAC_MODE_COOL + device.set_preset_mode(PRESET_ECO) + assert device.values.mode.data == PRESET_ECO + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT_COOL + device.values.mode = None + device.set_hvac_mode("test_set_failes") + assert device.values.mode is None + device.set_preset_mode("test_set_failes") + assert device.values.mode is None def test_operation_value_set_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.values.mode.data == "Off" - device.set_hvac_mode(HVAC_MODE_HEAT) assert device.values.mode.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) assert device.values.mode.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) assert device.values.mode.data == "Off" + device.set_preset_mode(PRESET_BOOST) + assert device.values.mode.data == "Full Power" + device.set_preset_mode(PRESET_ECO) + assert device.values.mode.data == "eco" + + +def test_operation_value_set_unknown(device_unknown): + """Test values changed for climate device. Unknown.""" + device = device_unknown + assert device.values.mode.data == "Heat" + device.set_preset_mode("Abcdefg") + assert device.values.mode.data == "Abcdefg" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT_COOL + + +def test_operation_value_set_heat_cool(device_heat_cool): + """Test values changed for climate device. Heat/Cool only.""" + device = device_heat_cool + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_preset_mode("Heat Eco") + assert device.values.mode.data == "Heat Eco" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_HEAT + device.set_preset_mode("Cool Eco") + assert device.values.mode.data == "Cool Eco" + device.set_preset_mode(PRESET_NONE) + assert device.values.mode.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -143,6 +306,9 @@ def test_fan_mode_value_set(device): assert device.values.fan_mode.data == "test2" device.set_fan_mode("test_fan_set") assert device.values.fan_mode.data == "test_fan_set" + device.values.fan_mode = None + device.set_fan_mode("test_fan_set_failes") + assert device.values.fan_mode is None def test_target_value_changed(device): @@ -163,25 +329,85 @@ def test_temperature_value_changed(device): def test_operation_value_changed(device): """Test values changed for climate device.""" - assert device.hvac_mode == "test1" - device.values.mode.data = "test_updated" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = HVAC_MODE_COOL + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == PRESET_NONE + device.values.mode.data = HVAC_MODE_OFF + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_OFF + assert device.preset_mode == PRESET_NONE + device.values.mode = None + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_preset(device_mapping): + """Test preset changed for climate device.""" + device = device_mapping + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = PRESET_ECO value_changed(device.values.mode) - assert device.hvac_mode == "test_updated" + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_ECO def test_operation_value_changed_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.hvac_mode == "off" - device.values.mode.data = "Heat" - value_changed(device.values.mode) assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Off" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_OFF + assert device.preset_mode == PRESET_NONE device.values.mode.data = "Cool" value_changed(device.values.mode) assert device.hvac_mode == HVAC_MODE_COOL - device.values.mode.data = "Off" + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_mapping_preset(device_mapping): + """Test values changed for climate device. Mapping with presets.""" + device = device_mapping + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Full Power" value_changed(device.values.mode) - assert device.hvac_mode == HVAC_MODE_OFF + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_BOOST + device.values.mode = None + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == PRESET_NONE + + +def test_operation_value_changed_unknown(device_unknown): + """Test preset changed for climate device. Unknown.""" + device = device_unknown + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Abcdefg" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT_COOL + assert device.preset_mode == "Abcdefg" + + +def test_operation_value_changed_heat_cool(device_heat_cool): + """Test preset changed for climate device. Heat/Cool only.""" + device = device_heat_cool + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == PRESET_NONE + device.values.mode.data = "Cool Eco" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_COOL + assert device.preset_mode == "Cool Eco" + device.values.mode.data = "Heat Eco" + value_changed(device.values.mode) + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.preset_mode == "Heat Eco" def test_fan_mode_value_changed(device): @@ -190,3 +416,29 @@ def test_fan_mode_value_changed(device): device.values.fan_mode.data = "test_updated_fan" value_changed(device.values.fan_mode) assert device.fan_mode == "test_updated_fan" + + +def test_hvac_action_value_changed(device): + """Test values changed for climate device.""" + assert device.hvac_action == CURRENT_HVAC_HEAT + device.values.operating_state.data = CURRENT_HVAC_COOL + value_changed(device.values.operating_state) + assert device.hvac_action == CURRENT_HVAC_COOL + + +def test_hvac_action_value_changed_mapping(device_mapping): + """Test values changed for climate device.""" + device = device_mapping + assert device.hvac_action == CURRENT_HVAC_HEAT + device.values.operating_state.data = "cooling" + value_changed(device.values.operating_state) + assert device.hvac_action == CURRENT_HVAC_COOL + + +def test_hvac_action_value_changed_unknown(device_unknown): + """Test values changed for climate device.""" + device = device_unknown + assert device.hvac_action == "test4" + device.values.operating_state.data = "another_hvac_action" + value_changed(device.values.operating_state) + assert device.hvac_action == "another_hvac_action" From e001b1243086202e5e5206b5875ec172a49e9b43 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 3 Aug 2019 18:49:34 +0200 Subject: [PATCH 015/213] Add PRESET_AWAY to HomematicIP Cloud climate (#25641) * enable climate away_mode and home.refresh * Add Party eco modes --- .../components/homematicip_cloud/climate.py | 19 +++++++++++++++---- .../components/homematicip_cloud/hap.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 53e7403ce56ab5..794a8b44cbcdd5 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -5,16 +5,19 @@ from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup from homematicip.aio.home import AsyncHome +from homematicip.base.enums import AbsenceType +from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -116,9 +119,17 @@ def preset_mode(self): if self._device.boostMode: return PRESET_BOOST if self._device.controlMode == HMIP_ECO_CM: - return PRESET_ECO - - return None + absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType + if absence_type == AbsenceType.VACATION: + return PRESET_AWAY + if absence_type in [ + AbsenceType.PERIOD, + AbsenceType.PERMANENT, + AbsenceType.PARTY, + ]: + return PRESET_ECO + + return PRESET_NONE @property def preset_modes(self): diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 7418aa94d89666..23973efb07b1c7 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -110,10 +110,13 @@ def async_update(self, *args, **kwargs): Triggered when the HMIP HOME_CHANGED event has fired. There are several occasions for this event to happen. - We are only interested to check whether the access point + 1. We are interested to check whether the access point is still connected. If not, device state changes cannot be forwarded to hass. So if access point is disconnected all devices are set to unavailable. + 2. We need to update home including devices and groups after a reconnect. + 3. We need to update home without devices and groups in all other cases. + """ if not self.home.connected: _LOGGER.error("HMIP access point has lost connection with the cloud") @@ -127,6 +130,12 @@ def async_update(self, *args, **kwargs): job = self.hass.async_create_task(self.get_state()) job.add_done_callback(self.get_state_finished) + self._accesspoint_connected = True + else: + # Update home with the given json from arg[0], + # without devices and groups. + + self.home.update_home_only(args[0]) async def get_state(self): """Update HMIP state and tell Home Assistant.""" From b40d324e0ef7f744ff71c32ec945a2d44fe801fc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Aug 2019 10:13:00 +0200 Subject: [PATCH 016/213] UniFi - allow configuration to not track clients or devices (#25642) * Allow configuration to not track clients or devices --- homeassistant/components/unifi/__init__.py | 4 ++ homeassistant/components/unifi/const.py | 2 + .../components/unifi/device_tracker.py | 70 +++++++++++-------- tests/components/unifi/test_device_tracker.py | 39 ++++++++++- 4 files changed, 83 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 726d479308516c..f4df139001d0fc 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,6 +11,8 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -28,6 +30,8 @@ vol.Optional(CONF_BLOCK_CLIENT, default=[]): vol.All( cv.ensure_list, [cv.string] ), + vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, + vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, vol.Optional(CONF_DETECTION_TIME): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 383b018264a80c..1295849704c3f5 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -13,6 +13,8 @@ CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" CONF_SSID_FILTER = "ssid_filter" ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 4046f5f63d2235..ce5a1a7f608672 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -28,6 +28,8 @@ ATTR_MANUFACTURER, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -154,46 +156,52 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - for client_id in controller.api.clients: + if not controller.unifi_config.get(CONF_DONT_TRACK_CLIENTS, False): - if client_id in tracked: - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", - tracked[client_id].entity_id, - tracked[client_id].client.mac, - ) - tracked[client_id].async_schedule_update_ha_state() - continue - - client = controller.api.clients[client_id] + for client_id in controller.api.clients: - if ( - not client.is_wired - and CONF_SSID_FILTER in controller.unifi_config - and client.essid not in controller.unifi_config[CONF_SSID_FILTER] - ): - continue + if client_id in tracked: + LOGGER.debug( + "Updating UniFi tracked client %s (%s)", + tracked[client_id].entity_id, + tracked[client_id].client.mac, + ) + tracked[client_id].async_schedule_update_ha_state() + continue - tracked[client_id] = UniFiClientTracker(client, controller) - new_tracked.append(tracked[client_id]) - LOGGER.debug("New UniFi client tracker %s (%s)", client.hostname, client.mac) + client = controller.api.clients[client_id] - for device_id in controller.api.devices: + if ( + not client.is_wired + and CONF_SSID_FILTER in controller.unifi_config + and client.essid not in controller.unifi_config[CONF_SSID_FILTER] + ): + continue - if device_id in tracked: + tracked[client_id] = UniFiClientTracker(client, controller) + new_tracked.append(tracked[client_id]) LOGGER.debug( - "Updating UniFi tracked device %s (%s)", - tracked[device_id].entity_id, - tracked[device_id].device.mac, + "New UniFi client tracker %s (%s)", client.hostname, client.mac ) - tracked[device_id].async_schedule_update_ha_state() - continue - device = controller.api.devices[device_id] + if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): + + for device_id in controller.api.devices: + + if device_id in tracked: + LOGGER.debug( + "Updating UniFi tracked device %s (%s)", + tracked[device_id].entity_id, + tracked[device_id].device.mac, + ) + tracked[device_id].async_schedule_update_ha_state() + continue + + device = controller.api.devices[device_id] - tracked[device_id] = UniFiDeviceTracker(device, controller) - new_tracked.append(tracked[device_id]) - LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) + tracked[device_id] = UniFiDeviceTracker(device, controller) + new_tracked.append(tracked[device_id]) + LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) if new_tracked: async_add_entities(new_tracked) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 5accbb584b48a0..fb13bef42aa530 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -22,6 +22,7 @@ CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -211,7 +212,7 @@ async def test_tracked_devices(hass, mock_controller): await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == "unavailable" + assert device_1.state == STATE_UNAVAILABLE async def test_restoring_client(hass, mock_controller): @@ -243,3 +244,39 @@ async def test_restoring_client(hass, mock_controller): device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None + + +async def test_dont_track_clients(hass, mock_controller): + """Test dont track clients config works.""" + mock_controller.mock_client_responses.append([CLIENT_1]) + mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_CLIENTS: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "not_home" + + +async def test_dont_track_devices(hass, mock_controller): + """Test dont track devices config works.""" + mock_controller.mock_client_responses.append([CLIENT_1]) + mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_DEVICES: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "not_home" + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is None From d95c86e96462cc41e685cf56f230890d71537ef0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2019 16:32:43 -0700 Subject: [PATCH 017/213] Add preset to be away and eco (#25643) --- homeassistant/components/nest/climate.py | 29 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index ac13f2b004f278..dc4b0bd33ae823 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -61,7 +61,9 @@ "cooling": CURRENT_HVAC_COOL, } -PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO] +PRESET_AWAY_AND_ECO = "Away and Eco" + +PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO] def setup_platform(hass, config, add_entities, discovery_info=None): @@ -259,6 +261,9 @@ def hvac_modes(self): @property def preset_mode(self): """Return current preset mode.""" + if self._away and self._mode == NEST_MODE_ECO: + return PRESET_AWAY_AND_ECO + if self._away: return PRESET_AWAY @@ -277,15 +282,19 @@ def set_preset_mode(self, preset_mode): if preset_mode == self.preset_mode: return - if self._away: - self.structure.away = False - elif preset_mode == PRESET_AWAY: - self.structure.away = True - - if self.preset_mode == PRESET_ECO: - self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] - elif preset_mode == PRESET_ECO: - self.device.mode = NEST_MODE_ECO + need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO) + need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO) + is_away = self._away + is_eco = self._mode == NEST_MODE_ECO + + if is_away != need_away: + self.structure.away = need_away + + if is_eco != need_eco: + if need_eco: + self.device.mode = NEST_MODE_ECO + else: + self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] @property def fan_mode(self): From 476607787a807cd113def5741af8f6cf704d64e4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 2 Aug 2019 17:00:22 +0200 Subject: [PATCH 018/213] Revert flux_led to 0.89 (#25653) * Revert Black * Revert "Introduce support for color temperature (#25503)" This reverts commit e1d884a4841f853bb4247a76fca417c0b4f4e7c1. * Revert "Fix flux_led only-white controllers (#22210)" This reverts commit 48138189b3c24261fe62a78b6ec854c761d5ce63. * Revert "Fix MagicHome LEDs with flux_led component (#20733)" This reverts commit 1444a684e02fab99648f6e5daea9f28b6cf45c10. * Re-Black * Use mode detection for scanned bulbs --- homeassistant/components/flux_led/light.py | 138 ++++++++------------- 1 file changed, 55 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index cef0387111a5a0..23fdb38aa05f7b 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -2,8 +2,6 @@ import logging import socket import random -from asyncio import sleep -from functools import partial import voluptuous as vol @@ -13,14 +11,12 @@ ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, - ATTR_COLOR_TEMP, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, - SUPPORT_COLOR_TEMP, Light, PLATFORM_SCHEMA, ) @@ -38,9 +34,7 @@ DOMAIN = "flux_led" -SUPPORT_FLUX_LED = ( - SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR | SUPPORT_COLOR_TEMP -) +SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR MODE_RGB = "rgb" MODE_RGBW = "rgbw" @@ -49,11 +43,6 @@ # RGB value is ignored when this mode is specified. MODE_WHITE = "w" -# Constant color temp values for 2 flux_led special modes -# Warm-white and Cool-white. Details on #23704 -COLOR_TEMP_WARM_WHITE = 333 -COLOR_TEMP_COOL_WHITE = 250 - # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = "red_fade" EFFECT_GREEN_FADE = "green_fade" @@ -196,8 +185,6 @@ def __init__(self, device): self._custom_effect = device[CONF_CUSTOM_EFFECT] self._bulb = None self._error_reported = False - self._color = (0, 0, 100) - self._white_value = 0 def _connect(self): """Connect to Flux light.""" @@ -238,14 +225,14 @@ def is_on(self): def brightness(self): """Return the brightness of this light between 0..255.""" if self._mode == MODE_WHITE: - return self._white_value + return self.white_value - return int(self._color[2] / 100 * 255) + return self._bulb.brightness @property def hs_color(self): """Return the color property.""" - return self._color[0:2] + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -261,7 +248,7 @@ def supported_features(self): @property def white_value(self): """Return the white value of this light between 0..255.""" - return self._white_value + return self._bulb.getRgbw()[3] @property def effect_list(self): @@ -282,85 +269,75 @@ def effect(self): for effect, code in EFFECT_MAP.items(): if current_mode == code: return effect - return None - async def async_turn_on(self, **kwargs): - """Turn the specified or all lights on and wait for state.""" - await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) - # The bulb needs a bit to tell its new values, - # so we wait 1 second before updating - await sleep(1) + return None - def _turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - self._bulb.turnOn() + if not self.is_on: + self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - - if all( - item is None for item in [hs_color, brightness, effect, white, color_temp] - ): - return - # handle W only mode (use brightness instead of white value) - if self._mode == MODE_WHITE: - if brightness is not None: - self._bulb.setWarmWhite255(brightness) + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning( + "RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb" + ) + + # Random color effect + if effect == EFFECT_RANDOM: + self._bulb.setRgb( + random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) + ) return - # handle effects - if effect is not None: - # Random color effect - if effect == EFFECT_RANDOM: - self._bulb.setRgb( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), + if effect == EFFECT_CUSTOM: + if self._custom_effect: + self._bulb.setCustomPattern( + self._custom_effect[CONF_COLORS], + self._custom_effect[CONF_SPEED_PCT], + self._custom_effect[CONF_TRANSITION], ) - elif effect == EFFECT_CUSTOM: - if self._custom_effect: - self._bulb.setCustomPattern( - self._custom_effect[CONF_COLORS], - self._custom_effect[CONF_SPEED_PCT], - self._custom_effect[CONF_TRANSITION], - ) - # Effect selection - elif effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) return - # handle special modes - if color_temp is not None: - if brightness is None: - brightness = self.brightness - if color_temp == COLOR_TEMP_WARM_WHITE: - self._bulb.setRgbw(w=brightness) - elif color_temp == COLOR_TEMP_COOL_WHITE: - self._bulb.setRgbw(w2=brightness) - else: - self._bulb.setRgbw(*color_util.color_temperature_to_rgb(color_temp)) + # Effect selection + if effect in EFFECT_MAP: + self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) return # Preserve current brightness on color/white level change - if hs_color is not None: - if brightness is None: - brightness = self.brightness - color = (hs_color[0], hs_color[1], brightness / 255 * 100) - elif brightness is not None: - color = (self._color[0], self._color[1], brightness / 255 * 100) + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + if white is None and self._mode == MODE_RGBW: + white = self.white_value + + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + # handle RGBW mode - if self._mode == MODE_RGBW: - if white is None: - self._bulb.setRgbw(*color_util.color_hsv_to_RGB(*color)) - else: - self._bulb.setRgbw(w=white) + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + # handle RGB mode else: - self._bulb.setRgb(*color_util.color_hsv_to_RGB(*color)) + self._bulb.setRgb(*tuple(rgb), brightness=brightness) def turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -380,10 +357,5 @@ def update(self): ) self._error_reported = True return + self._bulb.update_state(retry=2) - if self._mode != MODE_WHITE and self._bulb.getRgb() != (0, 0, 0): - color = self._bulb.getRgbw() - self._color = color_util.color_RGB_to_hsv(*color[0:3]) - self._white_value = color[3] - elif self._mode == MODE_WHITE: - self._white_value = self._bulb.getRgbw()[3] From 7a71669027f9667552c403c938b7015012560bbc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Aug 2019 23:51:06 +0200 Subject: [PATCH 019/213] Options to not track wired clients (#25669) --- homeassistant/components/unifi/__init__.py | 2 ++ homeassistant/components/unifi/const.py | 1 + .../components/unifi/device_tracker.py | 7 +++++++ tests/components/unifi/test_device_tracker.py | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index f4df139001d0fc..4ca6f68c301f60 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -13,6 +13,7 @@ CONF_DETECTION_TIME, CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -32,6 +33,7 @@ ), vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, + vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, vol.Optional(CONF_DETECTION_TIME): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 1295849704c3f5..b4864421cb9067 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -15,6 +15,7 @@ CONF_DETECTION_TIME = "detection_time" CONF_DONT_TRACK_CLIENTS = "dont_track_clients" CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONF_SSID_FILTER = "ssid_filter" ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ce5a1a7f608672..9c645a072a5a18 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -30,6 +30,7 @@ CONF_DETECTION_TIME, CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -178,6 +179,12 @@ def update_items(controller, async_add_entities, tracked): ): continue + if ( + controller.unifi_config.get(CONF_DONT_TRACK_WIRED_CLIENTS, False) + and client.is_wired + ): + continue + tracked[client_id] = UniFiClientTracker(client, controller) new_tracked.append(tracked[client_id]) LOGGER.debug( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index fb13bef42aa530..9fca9d21a5b95e 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -280,3 +280,21 @@ async def test_dont_track_devices(hass, mock_controller): device_1 = hass.states.get("device_tracker.device_1") assert device_1 is None + + +async def test_dont_track_wired_clients(hass, mock_controller): + """Test dont track wired clients config works.""" + mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + mock_controller.mock_device_responses.append({}) + mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True} + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "not_home" + + client_2 = hass.states.get("device_tracker.client_2") + assert client_2 is None From f8753a0c92bc9e8646e44eb315a3772c060451a7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 4 Aug 2019 16:11:28 -0600 Subject: [PATCH 020/213] Fix issue with incorrect Notion bridge IDs (#25683) * Fix issue with incorrect Notion bridge IDs * Less aggressive * Member comments --- homeassistant/components/notion/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index d2c45330bdb75c..62deb4999d9c36 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -235,7 +235,7 @@ def device_state_attributes(self) -> dict: @property def device_info(self): """Return device registry information for this entity.""" - bridge = self._notion.bridges[self._bridge_id] + bridge = self._notion.bridges.get(self._bridge_id, {}) sensor = self._notion.sensors[self._sensor_id] return { @@ -244,7 +244,7 @@ def device_info(self): "model": sensor["hardware_revision"], "name": sensor["name"], "sw_version": sensor["firmware_version"], - "via_device": (DOMAIN, bridge["hardware_id"]), + "via_device": (DOMAIN, bridge.get("hardware_id")), } @property @@ -271,7 +271,14 @@ async def _update_bridge_id(self): Sensors can move to other bridges based on signal strength, etc. """ sensor = self._notion.sensors[self._sensor_id] - if self._bridge_id == sensor["bridge"]["id"]: + + # If the sensor's bridge ID is the same as what we had before or if it points + # to a bridge that doesn't exist (which can happen due to a Notion API bug), + # return immediately: + if ( + self._bridge_id == sensor["bridge"]["id"] + or sensor["bridge"]["id"] not in self._notion.bridges + ): return self._bridge_id = sensor["bridge"]["id"] From 2925f9e57a1ee6f9c4004b0c322a09b7d45ef5e0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 4 Aug 2019 16:12:16 +0200 Subject: [PATCH 021/213] In some circumstances device.last_seen can be None (#25690) --- homeassistant/components/unifi/device_tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 9c645a072a5a18..8ab5140dc48e31 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -296,7 +296,9 @@ def is_connected(self): ) if ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) + self.device.last_seen + and dt_util.utcnow() + - dt_util.utc_from_timestamp(float(self.device.last_seen)) ) < detection_time: return True return False From 4e2094c89383b1901de204bda8fa136d193d6d6a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 4 Aug 2019 16:57:36 +0200 Subject: [PATCH 022/213] UniFi - reverse connectivity logic (#25691) * Make connectivity control in line with other implementations --- homeassistant/components/unifi/switch.py | 12 ++++++------ tests/components/unifi/test_switch.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 236ca02ab2d3b9..2b7965d1095b13 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -266,8 +266,8 @@ def unique_id(self): @property def is_on(self): - """Return true if client is blocked.""" - return self.client.blocked + """Return true if client is allowed to connect.""" + return not self.client.blocked @property def available(self): @@ -275,9 +275,9 @@ def available(self): return self.controller.available async def async_turn_on(self, **kwargs): - """Block client.""" - await self.controller.api.clients.async_block(self.client.mac) + """Turn on connectivity for client.""" + await self.controller.api.clients.async_unblock(self.client.mac) async def async_turn_off(self, **kwargs): - """Unblock client.""" - await self.controller.api.clients.async_unblock(self.client.mac) + """Turn off connectivity for client.""" + await self.controller.api.clients.async_block(self.client.mac) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 915646b9856f07..f84efa5dada4dd 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -342,11 +342,11 @@ async def test_switches(hass, mock_controller): blocked = hass.states.get("switch.block_client_1") assert blocked is not None - assert blocked.state == "on" + assert blocked.state == "off" unblocked = hass.states.get("switch.block_client_2") assert unblocked is not None - assert unblocked.state == "off" + assert unblocked.state == "on" async def test_new_client_discovered(hass, mock_controller): From 868c6f4f718194ee3d1bba5e03b7e3253410507a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 5 Aug 2019 00:13:27 +0200 Subject: [PATCH 023/213] Fix roku lxml requirement (#25696) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 6bdc1f6bf3d0bc..477bcb105f7f5b 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,7 +3,7 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/components/roku", "requirements": [ - "roku==3.0.0" + "roku==3.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index d70c0602c4c2d7..14367506aa862b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1640,7 +1640,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.0.0 +roku==3.1 # homeassistant.components.roomba roombapy==1.3.1 From 6e61b21919276403871b7e8f49482df6d8baea9c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Aug 2019 22:50:46 -0700 Subject: [PATCH 024/213] Bumped version to 0.97.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4de4048d4f928c..be27daee2a0ce7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 387323a8c1caa670954482b4390070b3314cd3d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Aug 2019 22:31:57 -0700 Subject: [PATCH 025/213] Updated frontend to 20190805.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4cea624b4cd072..6d25e846db93c8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190804.0" + "home-assistant-frontend==20190805.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13c5b1a01443b8..c51b13100db8db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 14367506aa862b..895441862bc78d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ hole==0.3.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d71d23b74ff7a..2f1177f0be5a31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ hdate==0.8.8 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190804.0 +home-assistant-frontend==20190805.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From e1c23b168620840b402a00d5a38d6c0998e72233 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 2 Aug 2019 01:43:08 +0200 Subject: [PATCH 026/213] Add HmIP-SCI to Homematic IP Cloud, Fix HmIP-SWDM (#25639) * Add HmIP-SCI to Homematic IP Cloud * Bump upstream dependency * Fix HmIP-SWDM --- .../components/homematicip_cloud/binary_sensor.py | 9 +++++++-- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 9580b803596483..7bb7718f0b3584 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homematicip.aio.device import ( + AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, AsyncMotionDetectorIndoor, @@ -10,6 +11,7 @@ AsyncPresenceDetectorIndoor, AsyncRotaryHandleSensor, AsyncShutterContact, + AsyncShutterContactMagnetic, AsyncSmokeDetector, AsyncWaterSensor, AsyncWeatherSensor, @@ -63,9 +65,12 @@ async def async_setup_entry( home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, AsyncFullFlushContactInterface): + if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): devices.append(HomematicipContactInterface(home, device)) - if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): + if isinstance( + device, + (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), + ): devices.append(HomematicipShutterContact(home, device)) if isinstance( device, diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b679130ce05bf4..ee0d2cb1271bf3 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ - "homematicip==0.10.9" + "homematicip==0.10.10" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 895441862bc78d..3cbac9098455cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -631,7 +631,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.9 +homematicip==0.10.10 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f1177f0be5a31..baa0763d38ecf5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -175,7 +175,7 @@ home-assistant-frontend==20190805.0 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.9 +homematicip==0.10.10 # homeassistant.components.google # homeassistant.components.remember_the_milk From b011dd0b0229cf0520fe430f2b1c77a700fdf4cf Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Mon, 5 Aug 2019 16:15:42 -0500 Subject: [PATCH 027/213] Bump envoy_reader to 0.8.6, fix missing dependency (#25679) * Bump envoy_reader to 0.8.6, fix missing dependency * Bump envoy_reader to 0.8.6, fix missing dependency --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 60f252c59a6f89..86d2d69cf9b47d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase envoy", "documentation": "https://www.home-assistant.io/components/enphase_envoy", "requirements": [ - "envoy_reader==0.8" + "envoy_reader==0.8.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 3cbac9098455cf..e986776fdb114a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -442,7 +442,7 @@ env_canada==0.0.20 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.8 +envoy_reader==0.8.6 # homeassistant.components.season ephem==3.7.6.0 From eceef82ffa3352dd6607247fb2b7f169c6974847 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Aug 2019 14:04:20 -0700 Subject: [PATCH 028/213] Add service to reload scenes from configuration.yaml (#25680) * Allow reloading scenes * Update requirements * address comments * fix typing * fix tests * Update homeassistant/components/homeassistant/scene.py Co-Authored-By: Martin Hjelmare * Address comments --- .../components/homeassistant/scene.py | 68 +++++++++++++-- homeassistant/components/scene/__init__.py | 5 ++ homeassistant/helpers/entity_component.py | 9 +- homeassistant/helpers/entity_platform.py | 8 ++ homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + setup.py | 86 +++++++++---------- tests/components/homeassistant/test_scene.py | 30 +++++++ tests/helpers/test_entity_component.py | 2 +- 9 files changed, 153 insertions(+), 57 deletions(-) create mode 100644 tests/components/homeassistant/test_scene.py diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index de8a4dc88e7daf..66b0410964009d 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,5 +1,6 @@ """Allow users to set and activate scenes.""" from collections import namedtuple +import logging import voluptuous as vol @@ -11,12 +12,19 @@ CONF_PLATFORM, STATE_OFF, STATE_ON, + SERVICE_RELOAD, +) +from homeassistant.core import State, DOMAIN +from homeassistant import config as conf_util +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import async_get_integration +from homeassistant.helpers import ( + config_per_platform, + config_validation as cv, + entity_platform, ) -from homeassistant.core import State -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state -from homeassistant.components.scene import STATES, Scene - +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene PLATFORM_SCHEMA = vol.Schema( { @@ -37,19 +45,63 @@ ) SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - scene_config = config.get(STATES) + _process_scenes_config(hass, async_add_entities, config) + + # This platform can be loaded multiple times. Only first time register the service. + if hass.services.has_service(SCENE_DOMAIN, SERVICE_RELOAD): + return + + # Store platform for later. + platform = entity_platform.current_platform.get() + + async def reload_config(call): + """Reload the scene config.""" + try: + conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + integration = await async_get_integration(hass, SCENE_DOMAIN) + + conf = await conf_util.async_process_component_config(hass, conf, integration) + + if not conf or not platform: + return + + await platform.async_reset() + + # Extract only the config for the Home Assistant platform, ignore the rest. + for p_type, p_config in config_per_platform(conf, SCENE_DOMAIN): + if p_type != DOMAIN: + continue + + _process_scenes_config(hass, async_add_entities, p_config) + + hass.helpers.service.async_register_admin_service( + SCENE_DOMAIN, SERVICE_RELOAD, reload_config + ) + + +def _process_scenes_config(hass, async_add_entities, config): + """Process multiple scenes and add them.""" + scene_config = config[STATES] + + # Check empty list + if not scene_config: + return async_add_entities( - HomeAssistantScene(hass, _process_config(scene)) for scene in scene_config + HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config ) - return True -def _process_config(scene_config): +def _process_scene_config(scene_config): """Process passed in config into a format to work with. Async friendly. diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 0d00c2c5ea2b15..5ddb1116d8f421 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ import voluptuous as vol +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import Entity @@ -60,6 +61,10 @@ async def async_setup(hass, config): component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) + # Ensure Home Assistant platform always loaded. + await component.async_setup_platform( + HA_DOMAIN, {"platform": "homeasistant", STATES: []} + ) async def async_handle_scene_service(service): """Handle calls to the switch services.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ed1b41a0abdd31..b28beeaea72bba 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -114,7 +114,7 @@ async def async_setup(self, config): # Look in config for Domain, Domain 2, Domain 3 etc and load them tasks = [] for p_type, p_config in config_per_platform(config, self.domain): - tasks.append(self._async_setup_platform(p_type, p_config)) + tasks.append(self.async_setup_platform(p_type, p_config)) if tasks: await asyncio.wait(tasks) @@ -123,7 +123,7 @@ async def async_setup(self, config): # Refer to: homeassistant.components.discovery.load_platform() async def component_platform_discovered(platform, info): """Handle the loading of a platform.""" - await self._async_setup_platform(platform, {}, info) + await self.async_setup_platform(platform, {}, info) discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered @@ -212,10 +212,13 @@ async def handle_service(call): self.hass.services.async_register(self.domain, name, handle_service, schema) - async def _async_setup_platform( + async def async_setup_platform( self, platform_type, platform_config, discovery_info=None ): """Set up a platform for this component.""" + if self.config is None: + raise RuntimeError("async_setup needs to be called first") + platform = await async_prepare_setup_platform( self.hass, self.config, self.domain, platform_type ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5012f578106a19..ea71828f21a6d8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,5 +1,7 @@ """Class to manage the entities for a single platform.""" import asyncio +from contextvars import ContextVar +from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id @@ -127,6 +129,7 @@ async def _async_setup_platform(self, async_create_setup_task, tries=0): async_create_setup_task creates a coroutine that sets up platform. """ + current_platform.set(self) logger = self.logger hass = self.hass full_name = "{}.{}".format(self.domain, self.platform_name) @@ -457,3 +460,8 @@ async def _update_entity_states(self, now): if tasks: await asyncio.wait(tasks) + + +current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( + "current_platform", default=None +) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c51b13100db8db..a1148063aeea39 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,6 +7,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.16 diff --git a/requirements_all.txt b/requirements_all.txt index e986776fdb114a..1fd445ea570e5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ async_timeout==3.0.1 attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 +contextvars==2.4;python_version<"3.7" importlib-metadata==0.18 jinja2>=2.10.1 PyJWT==1.7.1 diff --git a/setup.py b/setup.py index 14162a86c12905..da50b5f988c15c 100755 --- a/setup.py +++ b/setup.py @@ -5,55 +5,55 @@ import homeassistant.const as hass_const -PROJECT_NAME = 'Home Assistant' -PROJECT_PACKAGE_NAME = 'homeassistant' -PROJECT_LICENSE = 'Apache License 2.0' -PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) -PROJECT_URL = 'https://home-assistant.io/' -PROJECT_EMAIL = 'hello@home-assistant.io' +PROJECT_NAME = "Home Assistant" +PROJECT_PACKAGE_NAME = "homeassistant" +PROJECT_LICENSE = "Apache License 2.0" +PROJECT_AUTHOR = "The Home Assistant Authors" +PROJECT_COPYRIGHT = " 2013-{}, {}".format(dt.now().year, PROJECT_AUTHOR) +PROJECT_URL = "https://home-assistant.io/" +PROJECT_EMAIL = "hello@home-assistant.io" -PROJECT_GITHUB_USERNAME = 'home-assistant' -PROJECT_GITHUB_REPOSITORY = 'home-assistant' +PROJECT_GITHUB_USERNAME = "home-assistant" +PROJECT_GITHUB_REPOSITORY = "home-assistant" -PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) -GITHUB_PATH = '{}/{}'.format( - PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) -GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) +PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) +GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) +GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) -DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) +DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, hass_const.__version__) PROJECT_URLS = { - 'Bug Reports': '{}/issues'.format(GITHUB_URL), - 'Dev Docs': 'https://developers.home-assistant.io/', - 'Discord': 'https://discordapp.com/invite/c5DvZ4e', - 'Forum': 'https://community.home-assistant.io/', + "Bug Reports": "{}/issues".format(GITHUB_URL), + "Dev Docs": "https://developers.home-assistant.io/", + "Discord": "https://discordapp.com/invite/c5DvZ4e", + "Forum": "https://community.home-assistant.io/", } -PACKAGES = find_packages(exclude=['tests', 'tests.*']) +PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - 'aiohttp==3.5.4', - 'astral==1.10.1', - 'async_timeout==3.0.1', - 'attrs==19.1.0', - 'bcrypt==3.1.7', - 'certifi>=2019.6.16', - 'importlib-metadata==0.18', - 'jinja2>=2.10.1', - 'PyJWT==1.7.1', + "aiohttp==3.5.4", + "astral==1.10.1", + "async_timeout==3.0.1", + "attrs==19.1.0", + "bcrypt==3.1.7", + "certifi>=2019.6.16", + 'contextvars==2.4;python_version<"3.7"', + "importlib-metadata==0.18", + "jinja2>=2.10.1", + "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - 'cryptography==2.7', - 'pip>=8.0.3', - 'python-slugify==3.0.2', - 'pytz>=2019.01', - 'pyyaml==5.1.1', - 'requests==2.22.0', - 'ruamel.yaml==0.15.99', - 'voluptuous==0.11.5', - 'voluptuous-serialize==2.1.0', + "cryptography==2.7", + "pip>=8.0.3", + "python-slugify==3.0.2", + "pytz>=2019.01", + "pyyaml==5.1.1", + "requests==2.22.0", + "ruamel.yaml==0.15.99", + "voluptuous==0.11.5", + "voluptuous-serialize==2.1.0", ] -MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) +MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( name=PROJECT_PACKAGE_NAME, @@ -67,11 +67,7 @@ include_package_data=True, zip_safe=False, install_requires=REQUIRES, - python_requires='>={}'.format(MIN_PY_VERSION), - test_suite='tests', - entry_points={ - 'console_scripts': [ - 'hass = homeassistant.__main__:main' - ] - }, + python_requires=">={}".format(MIN_PY_VERSION), + test_suite="tests", + entry_points={"console_scripts": ["hass = homeassistant.__main__:main"]}, ) diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py new file mode 100644 index 00000000000000..02c018a0b49b01 --- /dev/null +++ b/tests/components/homeassistant/test_scene.py @@ -0,0 +1,30 @@ +"""Test Home Assistant scenes.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component + + +async def test_reload_config_service(hass): + """Test the reload config service.""" + assert await async_setup_component(hass, "scene", {}) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}}, + ), patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call("scene", "reload", blocking=True) + await hass.async_block_till_done() + + assert hass.states.get("scene.hallo") is None + assert hass.states.get("scene.bye") is not None diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3dd6ca8b55f67e..0d52f430ff5d23 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -116,7 +116,7 @@ async def test_setup_recovers_when_setup_raises(hass): @asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent" "._async_setup_platform", + "homeassistant.helpers.entity_component.EntityComponent" ".async_setup_platform", return_value=mock_coro(), ) @asynctest.patch( From fee1568a856497a4f9718c0ec05a88be536692fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Aug 2019 23:24:54 -0700 Subject: [PATCH 029/213] Update HTTP defaults (#25702) * Update HTTP defaults * Fix tests --- homeassistant/components/http/__init__.py | 4 +++- homeassistant/components/http/cors.py | 3 +++ tests/components/http/test_init.py | 9 +++++++++ tests/scripts/test_check_config.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 84c7d15a5804fd..6d31c3fc700629 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -51,6 +51,8 @@ DEFAULT_SERVER_HOST = "0.0.0.0" DEFAULT_DEVELOPMENT = "0" +# To be able to load custom cards. +DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 @@ -91,7 +93,7 @@ def api_password_deprecated(value): vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All( + vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 5c24ecbebed1e5..19fe88c5cde644 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -45,6 +45,9 @@ def _allow_cors(route, config=None): path = path.canonical + if path.startswith("/api/hassio_ingress/"): + return + if path in cors_added: return diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index a3837a0b745198..d8e613df6df01a 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -232,3 +232,12 @@ async def test_ssl_profile_change_modern(hass): await hass.async_block_till_done() assert len(mock_context.mock_calls) == 1 + + +async def test_cors_defaults(hass): + """Test the CORS default settings.""" + with patch("homeassistant.components.http.setup_cors") as mock_setup: + assert await async_setup_component(hass, "http", {}) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 98c634cd400adc..a07b812bc96830 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -103,7 +103,7 @@ def test_secrets(isfile_patch, loop): assert res["components"].keys() == {"homeassistant", "http"} assert res["components"]["http"] == { "api_password": "abc123", - "cors_allowed_origins": [], + "cors_allowed_origins": ["https://cast.home-assistant.io"], "ip_ban_enabled": True, "login_attempts_threshold": -1, "server_host": "0.0.0.0", From 27cfda11f7341dc85fcc4fa45f1dfe215d0aabf2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Aug 2019 07:00:06 +0200 Subject: [PATCH 030/213] UniFi - handle device not having a name (#25713) * Handle device not having a name --- .../components/unifi/device_tracker.py | 20 ++++++++++++++----- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 8ab5140dc48e31..d0c2684ff536a9 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -188,7 +188,9 @@ def update_items(controller, async_add_entities, tracked): tracked[client_id] = UniFiClientTracker(client, controller) new_tracked.append(tracked[client_id]) LOGGER.debug( - "New UniFi client tracker %s (%s)", client.hostname, client.mac + "New UniFi client tracker %s (%s)", + client.name or client.hostname, + client.mac, ) if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): @@ -208,7 +210,11 @@ def update_items(controller, async_add_entities, tracked): tracked[device_id] = UniFiDeviceTracker(device, controller) new_tracked.append(tracked[device_id]) - LOGGER.debug("New UniFi device tracker %s (%s)", device.name, device.mac) + LOGGER.debug( + "New UniFi device tracker %s (%s)", + device.name or device.model, + device.mac, + ) if new_tracked: async_add_entities(new_tracked) @@ -311,7 +317,7 @@ def source_type(self): @property def name(self) -> str: """Return the name of the device.""" - return self.device.name + return self.device.name or self.device.model @property def unique_id(self) -> str: @@ -326,14 +332,18 @@ def available(self) -> bool: @property def device_info(self): """Return a device description for device registry.""" - return { + info = { "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, "manufacturer": ATTR_MANUFACTURER, "model": self.device.model, - "name": self.device.name, "sw_version": self.device.version, } + if self.device.name: + info["name"] = self.device.name + + return info + @property def device_state_attributes(self): """Return the device state attributes.""" diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e849fd34d2582f..bcee022e1c4167 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==9" + "aiounifi==10" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1fd445ea570e5a..3e46f05b5a6c44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==9 +aiounifi==10 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baa0763d38ecf5..0fba87e7d4bc3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==9 +aiounifi==10 # homeassistant.components.wwlln aiowwlln==1.0.0 From d702b17a4f14736a97c35b2239dc1591379758ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Aug 2019 09:00:20 -0700 Subject: [PATCH 031/213] Bumped version to 0.97.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index be27daee2a0ce7..a00925c0025d9c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 3a78250cad1b594a8863c18146e4d11a08986ae0 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 6 Aug 2019 19:03:52 +0300 Subject: [PATCH 032/213] Bump hdate==0.9.0 (use pytz instead of dateutil) (#25726) Use new hdate version of library which uses pytz for timezones. dateutil expects /usr/share/timezone files, as these are not available in the docker image and in HASSIO, the timezone offsets are broken. This should fix - #23032 - #18731 --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 5c3eee48ead2a0..fdc1d2943e6fe9 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Jewish calendar", "documentation": "https://www.home-assistant.io/components/jewish_calendar", "requirements": [ - "hdate==0.8.8" + "hdate==0.9.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3e46f05b5a6c44..5e583269562a39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -599,7 +599,7 @@ hass-nabucasa==0.16 hbmqtt==0.9.4 # homeassistant.components.jewish_calendar -hdate==0.8.8 +hdate==0.9.0 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fba87e7d4bc3c..e2da65e24cd547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -163,7 +163,7 @@ hass-nabucasa==0.16 hbmqtt==0.9.4 # homeassistant.components.jewish_calendar -hdate==0.8.8 +hdate==0.9.0 # homeassistant.components.workday holidays==0.9.11 From a1302a9dfbbcbbc64f72bb83b3b57b3fe9294b6a Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 6 Aug 2019 17:03:08 +0100 Subject: [PATCH 033/213] initial commit (#25731) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 13c77cd33fffc8..8b5f461c8afb08 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,7 +3,7 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/components/incomfort", "requirements": [ - "incomfort-client==0.3.0" + "incomfort-client==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5e583269562a39..06a7ada8200479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -665,7 +665,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.0 +incomfort-client==0.3.1 # homeassistant.components.influxdb influxdb==5.2.0 From 52de2f4ffb6431c6fb2e0ab6b41288a05c3b6887 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Aug 2019 11:46:07 -0700 Subject: [PATCH 034/213] Revert emulated hue changes (#25732) --- .../components/emulated_hue/hue_api.py | 26 ++++++------------- tests/components/emulated_hue/test_hue_api.py | 14 ---------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 6d59d777e8b18e..1b08b43c9af63b 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -562,25 +562,15 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if entity_features & SUPPORT_BRIGHTNESS: - return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } return { - "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, - "type": "On/off light", + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", "name": config.get_entity_name(entity), "modelid": "HASS123", "uniqueid": entity.entity_id, diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 02f24f5afba701..57f29a4ef6180b 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -128,9 +128,6 @@ def hass_hue(loop, hass): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs ) - # create a lamp without brightness support - hass.states.async_set("light.no_brightness", "on", {}) - # Ceiling Fan is explicitly excluded from being exposed ceiling_fan_entity = hass.states.get("fan.ceiling_fan") attrs = dict(ceiling_fan_entity.attributes) @@ -221,17 +218,6 @@ def test_discover_lights(hue_client): assert "climate.ecobee" not in devices -@asyncio.coroutine -def test_light_without_brightness_supported(hass_hue, hue_client): - """Test that light without brightness is supported.""" - light_without_brightness_json = yield from perform_get_light_state( - hue_client, "light.no_brightness", 200 - ) - - assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True - assert light_without_brightness_json["type"] == "On/off light" - - @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" From 609118d3ac10101f531314ba6515bc1f95636901 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Aug 2019 23:55:36 +0200 Subject: [PATCH 035/213] Fix last seen not available on certain devices (#25735) --- homeassistant/components/unifi/device_tracker.py | 12 +++++++----- tests/components/unifi/test_device_tracker.py | 13 ++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index d0c2684ff536a9..89d3fce515ef7f 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -301,11 +301,10 @@ def is_connected(self): CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME ) - if ( - self.device.last_seen - and dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.device.last_seen)) - ) < detection_time: + if self.device.last_seen and ( + dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) + < detection_time + ): return True return False @@ -347,6 +346,9 @@ def device_info(self): @property def device_state_attributes(self): """Return the device state attributes.""" + if not self.device.last_seen: + return {} + attributes = {} attributes["upgradable"] = self.device.upgradable diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 9fca9d21a5b95e..0d8d631d8ffa62 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -73,6 +73,17 @@ "upgradable": False, "version": "4.0.42.10433", } +DEVICE_2 = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "ip": "10.0.1.1", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "device_1", + "type": "usw", + "version": "4.0.42.10433", +} CONTROLLER_DATA = { CONF_HOST: "mock-host", @@ -167,7 +178,7 @@ async def test_no_clients(hass, mock_controller): async def test_tracked_devices(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) - mock_controller.mock_device_responses.append([DEVICE_1]) + mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) mock_controller.unifi_config = {unifi_dt.CONF_SSID_FILTER: ["ssid"]} await setup_controller(hass, mock_controller) From 0f8f4f4b547ee387476a0d3c456cef219808567f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2019 09:25:10 -0700 Subject: [PATCH 036/213] Bumped version to 0.97.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a00925c0025d9c..4e9d5868f6cc38 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From a20c63141006d35bc029ec351ee2a6b9f99a0259 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2019 10:35:24 -0700 Subject: [PATCH 037/213] Update requirements --- script/gen_requirements_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 14cb165e6cfcd7..965f881b9c71fa 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -215,7 +215,7 @@ def core_requirements(): """Gather core requirements out of setup.py.""" with open("setup.py") as inp: reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1) - return re.findall(r"'(.*?)'", reqs_raw) + return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] def gather_recursive_requirements(domain, seen=None): From e35501222983753db1c6fed93cfe501af28b311d Mon Sep 17 00:00:00 2001 From: Dustin Essington Date: Fri, 9 Aug 2019 10:54:33 -0700 Subject: [PATCH 038/213] Update HIBP sensor to use API v3 and API Key (#25699) * Update HIBP sensor to use API v3 and API Key * ran black code formatter * fixed stray , that was invalid in multiple json formatters --- .../components/haveibeenpwned/manifest.json | 12 ++++----- .../components/haveibeenpwned/sensor.py | 27 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index f0b0561e170eac..40572f82ea80eb 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,8 +1,8 @@ { - "domain": "haveibeenpwned", - "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/components/haveibeenpwned", - "requirements": [], - "dependencies": [], - "codeowners": [] + "domain": "haveibeenpwned", + "name": "Haveibeenpwned", + "documentation": "https://www.home-assistant.io/components/haveibeenpwned", + "requirements": [], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index d78756b9543afe..ec43d9444a238c 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL, ATTR_ATTRIBUTION +from homeassistant.const import CONF_EMAIL, CONF_API_KEY, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -25,17 +25,21 @@ MIN_TIME_BETWEEN_FORCED_UPDATES = timedelta(seconds=5) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) -URL = "https://haveibeenpwned.com/api/v2/breachedaccount/" +URL = "https://haveibeenpwned.com/api/v3/breachedaccount/" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string])} + { + vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_API_KEY): cv.string, + } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the HaveIBeenPwned sensor.""" emails = config.get(CONF_EMAIL) - data = HaveIBeenPwnedData(emails) + api_key = config[CONF_API_KEY] + data = HaveIBeenPwnedData(emails, api_key) devices = [] for email in emails: @@ -125,13 +129,14 @@ def update(self): class HaveIBeenPwnedData: """Class for handling the data retrieval.""" - def __init__(self, emails): + def __init__(self, emails, api_key): """Initialize the data object.""" self._email_count = len(emails) self._current_index = 0 self.data = {} self._email = emails[0] self._emails = emails + self._api_key = api_key def set_next_email(self): """Set the next email to be looked up.""" @@ -146,16 +151,10 @@ def update_no_throttle(self): def update(self, **kwargs): """Get the latest data for current email from REST service.""" try: - url = "{}{}".format(URL, self._email) - + url = "{}{}?truncateResponse=false".format(URL, self._email) + header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key} _LOGGER.debug("Checking for breaches for email: %s", self._email) - - req = requests.get( - url, - headers={USER_AGENT: HA_USER_AGENT}, - allow_redirects=True, - timeout=5, - ) + req = requests.get(url, headers=header, allow_redirects=True, timeout=5) except requests.exceptions.RequestException: _LOGGER.error("Failed fetching data for %s", self._email) From ebf8d5fc662270fcc9acfc9b5ecc5efbcd9ae888 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Thu, 8 Aug 2019 21:59:33 +0100 Subject: [PATCH 039/213] Update Cisco Mobility Express module version (#25770) * Update manifest.json * Update requirements_all.txt --- homeassistant/components/cisco_mobility_express/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index 1d80076793dee5..abdd2400311f24 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -3,7 +3,7 @@ "name": "Cisco mobility express", "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", "requirements": [ - "ciscomobilityexpress==0.3.1" + "ciscomobilityexpress==0.3.3" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/requirements_all.txt b/requirements_all.txt index 06a7ada8200479..5404b514a58e59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,7 +321,7 @@ buienradar==1.0.1 caldav==0.6.1 # homeassistant.components.cisco_mobility_express -ciscomobilityexpress==0.3.1 +ciscomobilityexpress==0.3.3 # homeassistant.components.ciscospark ciscosparkapi==0.4.2 From 34b5083c278d25f6bdd6693606ac23a85643ba06 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 8 Aug 2019 14:43:53 -0500 Subject: [PATCH 040/213] Don't track unstable attributes (#25787) --- homeassistant/components/unifi/device_tracker.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 89d3fce515ef7f..d9f90de7888b98 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -41,11 +41,7 @@ DEVICE_ATTRIBUTES = [ "_is_guest_by_uap", - "ap_mac", "authorized", - "bssid", - "ccq", - "channel", "essid", "hostname", "ip", @@ -54,14 +50,11 @@ "is_wired", "mac", "name", - "noise", "noted", "oui", "qos_policy_applied", "radio", "radio_proto", - "rssi", - "signal", "site_id", "vlan", ] From 20e279a7ac1449d59054217e298cfeb7503eb5d5 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Fri, 9 Aug 2019 20:31:58 +0300 Subject: [PATCH 041/213] Fix deconz allow_clip_sensor and allow_deconz_groups options (#25811) --- homeassistant/components/deconz/gateway.py | 4 ++-- tests/components/deconz/test_binary_sensor.py | 10 +++++++--- tests/components/deconz/test_climate.py | 10 +++++++--- tests/components/deconz/test_light.py | 10 +++++++--- tests/components/deconz/test_sensor.py | 10 +++++++--- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8eca227f0cd004..0ed3ffd2a564ca 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -63,12 +63,12 @@ def master(self) -> bool: @property def allow_clip_sensor(self) -> bool: """Allow loading clip sensor from gateway.""" - return self.config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True) @property def allow_deconz_groups(self) -> bool: """Allow loading deCONZ groups from gateway.""" - return self.config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True) + return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True) async def async_update_device_registry(self): """Update device registry.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 4978a6f75d03bc..9eb408ba4f1790 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -31,14 +31,17 @@ ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" @@ -47,7 +50,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -56,6 +59,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 2f2bcbed255e8e..264c3b8761f550 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -39,14 +39,17 @@ } ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" @@ -59,7 +62,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): session = Mock(put=asynctest.CoroutineMock(return_value=response)) - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -68,6 +71,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(hass.loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 2d5ba57b6de0f3..77e983e34b4e5c 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -62,14 +62,17 @@ ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_deconz_groups=True): """Load the deCONZ light platform.""" @@ -78,7 +81,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups config_entry = config_entries.ConfigEntry( 1, @@ -87,6 +90,7 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index d881a87a6e6057..9c03f3e9a90e2d 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -75,14 +75,17 @@ ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", deconz.config_flow.CONF_BRIDGEID: "0123456789", deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +ENTRY_OPTIONS = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +} + async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" @@ -91,7 +94,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): loop = Mock() session = Mock() - ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor config_entry = config_entries.ConfigEntry( 1, @@ -100,6 +103,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) From e1fee1bd455dcc6a2f7da930fb33f8d3aa79c53b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Aug 2019 11:17:31 -0700 Subject: [PATCH 042/213] Bumped version to 0.97.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4e9d5868f6cc38..51549b8f07c918 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From e57ecc9d7dec1e6accc14c27dfc8d353d3a53306 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 9 Aug 2019 13:41:50 -0500 Subject: [PATCH 043/213] Fix brightness type (#25818) --- homeassistant/components/smartthings/light.py | 4 +++- tests/components/smartthings/test_light.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 9ec4634ab36eed..4bc3f4877907a4 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -133,7 +133,9 @@ async def async_update(self): """Update entity attributes when the device status has changed.""" # Brightness and transition if self._supported_features & SUPPORT_BRIGHTNESS: - self._brightness = convert_scale(self._device.status.level, 100, 255) + self._brightness = int( + convert_scale(self._device.status.level, 100, 255, 0) + ) # Color Temperature if self._supported_features & SUPPORT_COLOR_TEMP: self._color_temp = color_util.color_temperature_kelvin_to_mired( diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index b0f7268217c548..e9004031e7d286 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -84,6 +84,7 @@ async def test_entity_state(hass, light_devices): state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION ) + assert isinstance(state.attributes[ATTR_BRIGHTNESS], int) assert state.attributes[ATTR_BRIGHTNESS] == 255 # Color Dimmer 1 @@ -103,6 +104,7 @@ async def test_entity_state(hass, light_devices): ) assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes[ATTR_HS_COLOR] == (273.6, 55.0) + assert isinstance(state.attributes[ATTR_COLOR_TEMP], int) assert state.attributes[ATTR_COLOR_TEMP] == 222 @@ -191,7 +193,7 @@ async def test_turn_on_with_brightness(hass, light_devices): assert state is not None assert state.state == "on" # round-trip rounding error (expected) - assert state.attributes[ATTR_BRIGHTNESS] == 73.95 + assert state.attributes[ATTR_BRIGHTNESS] == 74 async def test_turn_on_with_minimal_brightness(hass, light_devices): @@ -216,7 +218,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): assert state is not None assert state.state == "on" # round-trip rounding error (expected) - assert state.attributes[ATTR_BRIGHTNESS] == 2.55 + assert state.attributes[ATTR_BRIGHTNESS] == 3 async def test_turn_on_with_color(hass, light_devices): From 77d984e9801f674c712746040f64705c3eb7d6f2 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 9 Aug 2019 21:08:35 +0200 Subject: [PATCH 044/213] Add script to install locale (#25791) --- Dockerfile | 1 + virtualization/Docker/Dockerfile.dev | 1 + virtualization/Docker/scripts/locales | 12 ++++++++++++ virtualization/Docker/setup_docker_prereqs | 5 +++++ 4 files changed, 19 insertions(+) create mode 100755 virtualization/Docker/scripts/locales diff --git a/Dockerfile b/Dockerfile index 09c1670754199e..a9e73699558b8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_SSOCR no #ENV INSTALL_DLIB no #ENV INSTALL_IPERF3 no +#ENV INSTALL_LOCALES no VOLUME /config diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 2191d8ad920e75..260a29cb3d09e3 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -14,6 +14,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_SSOCR no #ENV INSTALL_DLIB no #ENV INSTALL_IPERF3 no +#ENV INSTALL_LOCALES no VOLUME /config diff --git a/virtualization/Docker/scripts/locales b/virtualization/Docker/scripts/locales new file mode 100755 index 00000000000000..cbbe03415751c4 --- /dev/null +++ b/virtualization/Docker/scripts/locales @@ -0,0 +1,12 @@ +#!/bin/bash +# Sets up locales. + +# Stop on errors +set -e + +apt-get update +apt-get install -y --no-install-recommends locales + +# Set the locale +sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen +locale-gen diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 9f3fc81d0459af..62ac73d366ec90 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -9,6 +9,7 @@ INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" INSTALL_DLIB="${INSTALL_DLIB:-yes}" +INSTALL_LOCALES="${INSTALL_LOCALES:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -70,6 +71,10 @@ if [ "$INSTALL_DLIB" == "yes" ]; then pip3 install --no-cache-dir "dlib>=19.5" fi +if [ "$INSTALL_LOCALES" == "yes" ]; then + virtualization/Docker/scripts/locales +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From 48e42d8595db279979173fa8117d43990382faa3 Mon Sep 17 00:00:00 2001 From: Cameron Morris <636871+cameronrmorris@users.noreply.github.com> Date: Fri, 9 Aug 2019 20:05:05 -0400 Subject: [PATCH 045/213] Fix eco preset for Wink Air Conditioner (#25763) * Add preset support for device * Provide mappings between preset changes --- homeassistant/components/wink/climate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index ed2d24828025e7..38f25ef0a83912 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -27,6 +27,7 @@ SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_PRESET_MODE, PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS @@ -62,7 +63,7 @@ SUPPORT_FAN_THERMOSTAT = [FAN_AUTO, FAN_ON] SUPPORT_PRESET_THERMOSTAT = [PRESET_AWAY, PRESET_ECO] -SUPPORT_FLAGS_AC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE +SUPPORT_FLAGS_AC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE SUPPORT_FAN_AC = [FAN_HIGH, FAN_LOW, FAN_MEDIUM] SUPPORT_PRESET_AC = [PRESET_NONE, PRESET_ECO] @@ -415,10 +416,13 @@ def current_temperature(self): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" + if not self.wink.is_on(): + return PRESET_NONE + mode = self.wink.current_mode() if mode == "auto_eco": return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -436,7 +440,7 @@ def hvac_mode(self) -> str: wink_mode = self.wink.current_mode() if wink_mode == "auto_eco": - return HVAC_MODE_AUTO + return HVAC_MODE_COOL return WINK_HVAC_TO_HA.get(wink_mode) @property @@ -476,6 +480,8 @@ def set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_ECO: self.wink.set_operation_mode("auto_eco") + elif self.hvac_mode == HVAC_MODE_COOL and preset_mode == PRESET_NONE: + self.set_hvac_mode(HVAC_MODE_COOL) @property def target_temperature(self): From 0eb93db67ea132969b42bc0b1b5b3d566ffb4bb0 Mon Sep 17 00:00:00 2001 From: Brandon Davidson Date: Fri, 9 Aug 2019 13:20:26 -0700 Subject: [PATCH 046/213] Update pyvera to 0.3.3 (#25820) Fixes #24987 --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 5fddce7efe7f85..07ae7ab3d3698b 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/components/vera", "requirements": [ - "pyvera==0.3.2" + "pyvera==0.3.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 5404b514a58e59..01c213b8b4e42a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1566,7 +1566,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.2 +pyvera==0.3.3 # homeassistant.components.vesync pyvesync==1.1.0 From 9afb6c3876182cac717b92e7cbac4d6e71e59971 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 10 Aug 2019 15:49:29 +0200 Subject: [PATCH 047/213] Fix Netatmo climate issue (#25830) * Bump pyatmo to v2.2.1 * Fix issue 25778 --- homeassistant/components/netatmo/climate.py | 4 ++-- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 109f12a87fc40c..9656d4a37a451c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -109,8 +109,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = hass.data[DATA_NETATMO_AUTH] + home_data = HomeData(auth) try: - home_data = HomeData(auth) + home_data.setup() except pyatmo.NoDevice: return @@ -352,7 +353,6 @@ def __init__(self, auth, home=None): def get_home_ids(self): """Get all the home ids returned by NetAtmo API.""" - self.setup() if self.homedata is None: return [] for home_id in self.homedata.homes: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 66b0efc61ff5e0..82f32c34407cab 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/components/netatmo", "requirements": [ - "pyatmo==2.2.0" + "pyatmo==2.2.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 01c213b8b4e42a..4a7ef886714c93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1046,7 +1046,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.2.0 +pyatmo==2.2.1 # homeassistant.components.apple_tv pyatv==0.3.12 From 0c815ea84337c100c40ef0532486b95cda8c731d Mon Sep 17 00:00:00 2001 From: tombbo <53979375+tombbo@users.noreply.github.com> Date: Sat, 10 Aug 2019 22:24:03 +0200 Subject: [PATCH 048/213] Fix KNX Climate mode change callback (#25851) - fix KNX Climate not updating UI after receiving mode change telegram from KNX bus --- homeassistant/components/knx/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index a3183c3b34d025..07aac11b972ff4 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -209,6 +209,7 @@ async def after_update_callback(device): await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) + self.device.mode.register_device_updated_cb(after_update_callback) @property def name(self) -> str: From a061310e78965058c43dae1a6a943f79c6d355f5 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 10 Aug 2019 23:47:36 -0500 Subject: [PATCH 049/213] Always populate hvac_modes in SmartThings climate platform (#25859) * Always return list for hvac_modes * Use climate constants --- .../components/smartthings/climate.py | 13 +++++----- tests/components/smartthings/test_climate.py | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index e9fefeb2995157..bb307523e97084 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -228,35 +228,34 @@ async def async_update(self): self._hvac_mode = MODE_TO_STATE.get(thermostat_mode) if self._hvac_mode is None: _LOGGER.debug( - "Device %s (%s) returned an invalid" "hvac mode: %s", + "Device %s (%s) returned an invalid hvac mode: %s", self._device.label, self._device.device_id, thermostat_mode, ) + modes = set() supported_modes = self._device.status.supported_thermostat_modes if isinstance(supported_modes, Iterable): - operations = set() for mode in supported_modes: state = MODE_TO_STATE.get(mode) if state is not None: - operations.add(state) + modes.add(state) else: _LOGGER.debug( - "Device %s (%s) returned an invalid " - "supported thermostat mode: %s", + "Device %s (%s) returned an invalid supported thermostat mode: %s", self._device.label, self._device.device_id, mode, ) - self._hvac_modes = operations else: _LOGGER.debug( - "Device %s (%s) returned invalid supported " "thermostat modes: %s", + "Device %s (%s) returned invalid supported thermostat modes: %s", self._device.label, self._device.device_id, supported_modes, ) + self._hvac_modes = list(modes) @property def current_humidity(self): diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 01206ded06238b..c366761ea1f1bf 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -214,14 +214,14 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == "idle" - assert state.attributes[ATTR_HVAC_MODES] == { + assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - } + ] assert state.attributes[ATTR_FAN_MODE] == "auto" assert state.attributes[ATTR_FAN_MODES] == ["auto", "on"] assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20 # celsius @@ -239,12 +239,12 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): == SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) assert ATTR_HVAC_ACTIONS not in state.attributes - assert state.attributes[ATTR_HVAC_MODES] == { - HVAC_MODE_OFF, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_HEAT, + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_COOL, - } + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ] assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius @@ -260,13 +260,13 @@ async def test_thermostat_entity_state(hass, thermostat): | SUPPORT_TARGET_TEMPERATURE ) assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_HVAC_MODES] == { + assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - } + ] assert state.attributes[ATTR_FAN_MODE] == "on" assert state.attributes[ATTR_FAN_MODES] == ["auto", "on"] assert state.attributes[ATTR_TEMPERATURE] == 20 # celsius @@ -286,6 +286,7 @@ async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): assert state.state is STATE_UNKNOWN assert state.attributes[ATTR_TEMPERATURE] is None assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + assert state.attributes[ATTR_HVAC_MODES] == [] async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): @@ -295,7 +296,7 @@ async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): ) await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get("climate.buggy_thermostat") - assert state.attributes[ATTR_HVAC_MODES] == {"heat"} + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] async def test_air_conditioner_entity_state(hass, air_conditioner): From f03538f8666b021818b771ae950f08c849156460 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 11 Aug 2019 22:40:44 +0200 Subject: [PATCH 050/213] UniFi - Use state to know if device is online (#25876) --- homeassistant/components/unifi/device_tracker.py | 13 ++++++++----- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_device_tracker.py | 2 ++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index d9f90de7888b98..42a6f496a2ae3b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -294,7 +294,7 @@ def is_connected(self): CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME ) - if self.device.last_seen and ( + if self.device.state == 1 and ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) < detection_time ): @@ -339,15 +339,18 @@ def device_info(self): @property def device_state_attributes(self): """Return the device state attributes.""" - if not self.device.last_seen: + if self.device.state == 0: return {} attributes = {} - attributes["upgradable"] = self.device.upgradable - attributes["overheating"] = self.device.overheating - if self.device.has_fan: attributes["fan_level"] = self.device.fan_level + if self.device.overheating: + attributes["overheating"] = self.device.overheating + + if self.device.upgradable: + attributes["upgradable"] = self.device.upgradable + return attributes diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index bcee022e1c4167..d182806c4ac7b7 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/unifi", "requirements": [ - "aiounifi==10" + "aiounifi==11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4a7ef886714c93..423409b70544b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ aiopvapi==1.6.14 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==10 +aiounifi==11 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2da65e24cd547..26630c11f677bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aionotion==1.1.0 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==10 +aiounifi==11 # homeassistant.components.wwlln aiowwlln==1.0.0 diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 0d8d631d8ffa62..d5783e58818a4f 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -69,6 +69,7 @@ "model": "US16P150", "name": "device_1", "overheating": False, + "state": 1, "type": "usw", "upgradable": False, "version": "4.0.42.10433", @@ -81,6 +82,7 @@ "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "device_1", + "state": 0, "type": "usw", "version": "4.0.42.10433", } From 38412fd88027fa2403a788b80910834e5038bf4d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 12 Aug 2019 05:48:56 +0200 Subject: [PATCH 051/213] Fix issue with nuki new available state (#25881) --- homeassistant/components/nuki/lock.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 38e42fcc1b5cb3..31a655dfeddd93 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -144,10 +144,12 @@ def update(self): self._nuki_lock.update(aggressive=False) except requests.exceptions.RequestException: self._available = False - else: - self._name = self._nuki_lock.name - self._locked = self._nuki_lock.is_locked - self._battery_critical = self._nuki_lock.battery_critical + return + + self._available = self._nuki_lock.state != 255 + self._name = self._nuki_lock.name + self._locked = self._nuki_lock.is_locked + self._battery_critical = self._nuki_lock.battery_critical def lock(self, **kwargs): """Lock the device.""" From 38c67389b26a168c1440a6a6d491361f285086f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Aug 2019 20:49:31 -0700 Subject: [PATCH 052/213] Bumped version to 0.97.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 51549b8f07c918..7078ee62d07859 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 97 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 8d16446c13292e3bea783f773f473dc9787df74c Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 00:20:10 +0200 Subject: [PATCH 053/213] first commit of atome component --- homeassistant/components/atome/__init__.py | 0 homeassistant/components/atome/manifest.json | 9 + homeassistant/components/atome/sensor.py | 249 +++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 homeassistant/components/atome/__init__.py create mode 100644 homeassistant/components/atome/manifest.json create mode 100644 homeassistant/components/atome/sensor.py diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json new file mode 100644 index 00000000000000..272f4e135f9bed --- /dev/null +++ b/homeassistant/components/atome/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "energy", + "name": "Atome", + "documentation": "", + "dependencies": [], + "codeowners": [], + "requirements": [], + "homeassistant":"0.96.0" +} diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py new file mode 100644 index 00000000000000..4dc386c132fb2b --- /dev/null +++ b/homeassistant/components/atome/sensor.py @@ -0,0 +1,249 @@ +""" Linky Atome """ +import logging + +_LOGGER = logging.getLogger(__name__) + + +from datetime import timedelta + +import pickle +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_TIMEOUT, + STATE_UNAVAILABLE, + CONF_NAME, +) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +import homeassistant.helpers.config_validation as cv + +import requests + +KILOWATT_HOUR = "kWh" +DEFAULT_NAME = "atome" +DEFAULT_UNIT = "W" + +ATOME_COOKIE = "atome_cookies.pickle" +ATOME_USER_ID = "atome_user_id.pickle" +ATOME_USER_REFERENCE = "atome_user_reference.pickle" +SCAN_INTERVAL = timedelta(seconds=30) +DEFAULT_TIMEOUT = 10 + + +COOKIE_NAME = "PHPSESSID" +API_BASE_URI = "https://esoftlink.esoftthings.com" +API_ENDPOINT_LOGIN = "/api/user/login.json" +API_ENDPOINT_LIVE = "/measure/live.json" + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Linky sensor.""" + name = config.get(CONF_NAME) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + timeout = config.get(CONF_TIMEOUT) + + # # LOGIN + cookie_path = hass.config.path(ATOME_COOKIE) + user_id_path = hass.config.path(ATOME_USER_ID) + user_reference_path = hass.config.path(ATOME_USER_REFERENCE) + + atome = AtomeSensor( + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ) + user_id, user_reference = atome._login(username, password) + # # Login the user into the Atome API. + # payload = {"email": username, + # "plainPassword": password} + + # req = requests.post(API_BASE_URI + API_ENDPOINT_LOGIN, json=payload, headers={"content-type":"application/json"}) + # response_json = req.json() + # # _LOGGER.debug(response_json) + # session_cookie = req.cookies.get(COOKIE_NAME) + # user_id = str(response_json['id']) + # user_reference = response_json['subscriptions'][0]['reference'] + + # if session_cookie is None: + # _LOGGER.exception("Login unsuccessful. Check your credentials") + # return False + + # # store cookie + # with open(cookie_path, 'wb') as f: + # pickle.dump(session_cookie, f) + # # store user id + # with open(user_id_path, 'wb') as f: + # pickle.dump(user_id, f) + # # store user ref + # with open(user_reference_path, 'wb') as f: + # pickle.dump(user_reference, f) + + # _LOGGER.info("Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference) + # # /LOGIN + + add_entities( + [ + AtomeSensor( + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ) + ] + ) + return True + + +class AtomeSensor(Entity): + """Representation of a sensor entity for Linky.""" + + def __init__( + self, + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ): + """Initialize the sensor.""" + _LOGGER.debug("ATOME: INIT") + self._name = name + # self._unit = DEFAULT_UNIT + self._unit_of_measurement = DEFAULT_UNIT + + self._username = username + self._password = password + self._timeout = timeout + + self._cookie_path = cookie_path + self._user_id_path = user_id_path + self._user_reference_path = user_reference_path + + self._attributes = None + self._state = None + # self.update = Throttle(SCAN_INTERVAL)(self._update) + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name or DEFAULT_NAME + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def _load_file(self, filename): + with open(filename, "rb") as f: + return pickle.load(f) + + def _login(self, username, password): + + # Login the user into the Atome API. + payload = {"email": username, "plainPassword": password} + + req = requests.post( + API_BASE_URI + API_ENDPOINT_LOGIN, + json=payload, + headers={"content-type": "application/json"}, + timeout=self._timeout, + ) + response_json = req.json() + # _LOGGER.debug(response_json) + session_cookie = req.cookies.get(COOKIE_NAME) + user_id = str(response_json["id"]) + user_reference = response_json["subscriptions"][0]["reference"] + + if session_cookie is None: + _LOGGER.exception("Login unsuccessful. Check your credentials") + return False + + # store cookie + with open(self._cookie_path, "wb") as f: + pickle.dump(session_cookie, f) + # store user id + with open(self._user_id_path, "wb") as f: + pickle.dump(user_id, f) + # store user ref + with open(self._user_reference_path, "wb") as f: + pickle.dump(user_reference, f) + + _LOGGER.info( + "Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", + user_id, + user_reference, + ) + # /LOGIN + return user_id, user_reference + + def _get_data(self, url): + + cookie = self._load_file(self._cookie_path) + cookies = {COOKIE_NAME: cookie} + + req = requests.get(url, cookies=cookies, timeout=self._timeout) + values = req.json() + + if req.status_code == 302: + _LOGGER.warning("Unable to fetch Linky data: need to re-login! ") + + if req.status_code != 200: + _LOGGER.warning("Unable to fetch Linky data: %s %s ", req.status_code, url) + + return values + + @Throttle(SCAN_INTERVAL) + def update(self): + """Update device state.""" + _LOGGER.debug("ATOME: Starting update of Atome Data") + + user_id = self._load_file(self._user_id_path) + user_reference = self._load_file(self._user_reference_path) + + url = ( + API_BASE_URI + + "/api/subscription/" + + user_id + + "/" + + user_reference + + API_ENDPOINT_LIVE + ) + + values = self._get_data(url) + self._state = values["last"] + + # TODO + # getData + # login From 13898effdbc7fab87dbddd3ddb8675f7d099c8ba Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 00:49:11 +0200 Subject: [PATCH 054/213] sanatizing + fixed some errors handling --- homeassistant/components/atome/sensor.py | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 4dc386c132fb2b..e1f852dd461e5f 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -64,16 +64,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): user_id_path = hass.config.path(ATOME_USER_ID) user_reference_path = hass.config.path(ATOME_USER_REFERENCE) - atome = AtomeSensor( - name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, - ) - user_id, user_reference = atome._login(username, password) + # atome = AtomeSensor( + # name, + # username, + # password, + # timeout, + # cookie_path, + # user_id_path, + # user_reference_path, + # ) + # user_id, user_reference = atome._login(username, password) # # Login the user into the Atome API. # payload = {"email": username, # "plainPassword": password} @@ -148,7 +148,8 @@ def __init__( self._attributes = None self._state = None # self.update = Throttle(SCAN_INTERVAL)(self._update) - self.update() + # self.update() + self._login(username, password) @property def name(self): @@ -183,13 +184,14 @@ def _login(self, username, password): response_json = req.json() # _LOGGER.debug(response_json) session_cookie = req.cookies.get(COOKIE_NAME) - user_id = str(response_json["id"]) - user_reference = response_json["subscriptions"][0]["reference"] if session_cookie is None: _LOGGER.exception("Login unsuccessful. Check your credentials") return False + user_id = str(response_json["id"]) + user_reference = response_json["subscriptions"][0]["reference"] + # store cookie with open(self._cookie_path, "wb") as f: pickle.dump(session_cookie, f) From bc4c466554fed43ba35c39955ce7df8cd1303272 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:01:07 +0200 Subject: [PATCH 055/213] fixed re-login --- homeassistant/components/atome/sensor.py | 53 ++++-------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index e1f852dd461e5f..f5b53c7ebe6d91 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -33,6 +33,7 @@ ATOME_USER_ID = "atome_user_id.pickle" ATOME_USER_REFERENCE = "atome_user_reference.pickle" SCAN_INTERVAL = timedelta(seconds=30) +SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 @@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Linky sensor.""" + """Set up the sensor.""" name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -64,44 +65,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): user_id_path = hass.config.path(ATOME_USER_ID) user_reference_path = hass.config.path(ATOME_USER_REFERENCE) - # atome = AtomeSensor( - # name, - # username, - # password, - # timeout, - # cookie_path, - # user_id_path, - # user_reference_path, - # ) - # user_id, user_reference = atome._login(username, password) - # # Login the user into the Atome API. - # payload = {"email": username, - # "plainPassword": password} - - # req = requests.post(API_BASE_URI + API_ENDPOINT_LOGIN, json=payload, headers={"content-type":"application/json"}) - # response_json = req.json() - # # _LOGGER.debug(response_json) - # session_cookie = req.cookies.get(COOKIE_NAME) - # user_id = str(response_json['id']) - # user_reference = response_json['subscriptions'][0]['reference'] - - # if session_cookie is None: - # _LOGGER.exception("Login unsuccessful. Check your credentials") - # return False - - # # store cookie - # with open(cookie_path, 'wb') as f: - # pickle.dump(session_cookie, f) - # # store user id - # with open(user_id_path, 'wb') as f: - # pickle.dump(user_id, f) - # # store user ref - # with open(user_reference_path, 'wb') as f: - # pickle.dump(user_reference, f) - - # _LOGGER.info("Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference) - # # /LOGIN - add_entities( [ AtomeSensor( @@ -119,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AtomeSensor(Entity): - """Representation of a sensor entity for Linky.""" + """Representation of a sensor entity for Atome.""" def __init__( self, @@ -170,6 +133,7 @@ def _load_file(self, filename): with open(filename, "rb") as f: return pickle.load(f) + # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): # Login the user into the Atome API. @@ -203,7 +167,7 @@ def _login(self, username, password): pickle.dump(user_reference, f) _LOGGER.info( - "Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", + "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference, ) @@ -218,11 +182,12 @@ def _get_data(self, url): req = requests.get(url, cookies=cookies, timeout=self._timeout) values = req.json() - if req.status_code == 302: - _LOGGER.warning("Unable to fetch Linky data: need to re-login! ") + if req.status_code == 403: + self._login(self._username, self._password) + _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) if req.status_code != 200: - _LOGGER.warning("Unable to fetch Linky data: %s %s ", req.status_code, url) + _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) return values From bedf2cbfecf31d8fddca30105107e4889d904cb1 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:20:20 +0200 Subject: [PATCH 056/213] updated coverage for atom which relies on external API --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 248e242faaf605..07c86dd0894e19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,6 +50,7 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* + homeassistant/components/atome/sensor.py homeassistant/components/asuswrt/device_tracker.py homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py From 87a58586a6c2f10d713736d76447e9c1d1aae36c Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:20:42 +0200 Subject: [PATCH 057/213] Proper manifest for atome --- homeassistant/components/atome/manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 272f4e135f9bed..0fa73277b06ac3 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,9 +1,8 @@ { - "domain": "energy", + "domain": "atome", "name": "Atome", "documentation": "", "dependencies": [], "codeowners": [], - "requirements": [], - "homeassistant":"0.96.0" + "requirements": [] } From d6443c35f52a78b0a635d76a1f55f0660d55a3bd Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:21:02 +0200 Subject: [PATCH 058/213] Added default power class for Atome --- homeassistant/components/atome/sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f5b53c7ebe6d91..fe3042b282bd2a 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -25,9 +25,9 @@ import requests -KILOWATT_HOUR = "kWh" DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" +DEFAULT_CLASS = "power" ATOME_COOKIE = "atome_cookies.pickle" ATOME_USER_ID = "atome_user_id.pickle" @@ -99,6 +99,7 @@ def __init__( self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT + self._device_class = DEFAULT_CLASS self._username = username self._password = password @@ -124,6 +125,11 @@ def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the device class.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" @@ -182,6 +188,9 @@ def _get_data(self, url): req = requests.get(url, cookies=cookies, timeout=self._timeout) values = req.json() + if req.status_code == 302: + _LOGGER.warning("Unable to fetch Atome data: need to re-login! ") + if req.status_code == 403: self._login(self._username, self._password) _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) From 80f632c392c5a3130676125f5e58f0c5d554595a Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 15:30:43 +0200 Subject: [PATCH 059/213] flake8 rules are now respected --- homeassistant/components/atome/__init__.py | 1 + homeassistant/components/atome/sensor.py | 20 ++++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py index e69de29bb2d1d6..6f524606a817bf 100644 --- a/homeassistant/components/atome/__init__.py +++ b/homeassistant/components/atome/__init__.py @@ -0,0 +1 @@ +"""Support for Atome devices connected to a Linky Energy Meter.""" diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index fe3042b282bd2a..d5075bca2820bd 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,30 +1,22 @@ -""" Linky Atome """ +"""Linky Atome.""" import logging -_LOGGER = logging.getLogger(__name__) - - from datetime import timedelta import pickle import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout - -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_TIMEOUT, - STATE_UNAVAILABLE, - CONF_NAME, -) + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle - import homeassistant.helpers.config_validation as cv import requests + +_LOGGER = logging.getLogger(__name__) + DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" DEFAULT_CLASS = "power" From b4fec698132bbac6fc03c03577df8dfcaba9d121 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 16:45:25 +0200 Subject: [PATCH 060/213] some pylint fixes --- homeassistant/components/atome/sensor.py | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index d5075bca2820bd..f8f3092e0f8221 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -5,6 +5,7 @@ import pickle import voluptuous as vol +import requests from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,8 +13,6 @@ from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -import requests - _LOGGER = logging.getLogger(__name__) @@ -127,9 +126,9 @@ def state(self): """Return the state of the sensor.""" return self._state - def _load_file(self, filename): - with open(filename, "rb") as f: - return pickle.load(f) + def load_file(self, filename): + with open(filename, "rb") as file: + return pickle.load(file) # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -155,14 +154,14 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - with open(self._cookie_path, "wb") as f: - pickle.dump(session_cookie, f) + with open(self._cookie_path, "wb") as file: + pickle.dump(session_cookie, file) # store user id - with open(self._user_id_path, "wb") as f: - pickle.dump(user_id, f) + with open(self._user_id_path, "wb") as file: + pickle.dump(user_id, file) # store user ref - with open(self._user_reference_path, "wb") as f: - pickle.dump(user_reference, f) + with open(self._user_reference_path, "wb") as file: + pickle.dump(user_reference, file) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", @@ -174,7 +173,7 @@ def _login(self, username, password): def _get_data(self, url): - cookie = self._load_file(self._cookie_path) + cookie = self.load_file(self._cookie_path) cookies = {COOKIE_NAME: cookie} req = requests.get(url, cookies=cookies, timeout=self._timeout) @@ -197,8 +196,8 @@ def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = self._load_file(self._user_id_path) - user_reference = self._load_file(self._user_reference_path) + user_id = self.load_file(self._user_id_path) + user_reference = self.load_file(self._user_reference_path) url = ( API_BASE_URI @@ -211,7 +210,3 @@ def update(self): values = self._get_data(url) self._state = values["last"] - - # TODO - # getData - # login From 7f317b8f7b3c51c5898bd14db5ab40a20432db05 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 17:06:06 +0200 Subject: [PATCH 061/213] Again, some flake8 fixes... --- homeassistant/components/atome/sensor.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f8f3092e0f8221..08ad81de8b40e8 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,9 +127,15 @@ def state(self): return self._state def load_file(self, filename): + """Loads a file stored with pickle.""" with open(filename, "rb") as file: return pickle.load(file) + def save_file(self, content, filename): + """Saves a file stored with pickle.""" + with open(filename, "wb") as file: + pickle.dump(content, file) + # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -154,14 +160,11 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - with open(self._cookie_path, "wb") as file: - pickle.dump(session_cookie, file) + self.save_file(session_cookie, self._cookie_path) # store user id - with open(self._user_id_path, "wb") as file: - pickle.dump(user_id, file) + self.save_file(user_id, self._user_id_path) # store user ref - with open(self._user_reference_path, "wb") as file: - pickle.dump(user_reference, file) + self.save_file(user_reference, self._user_reference_path) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", From 10d63e46d7b654ed911bb2af7c92840b76b1eac0 Mon Sep 17 00:00:00 2001 From: ThaSiouL Date: Sun, 18 Aug 2019 17:08:26 +0200 Subject: [PATCH 062/213] Fritz device tracker: set 'scanning' log to debug (#26043) Changed the logging level for the polling of the fritz device tracker from info to debug. The message was logged up to 6 times per minute and bloated the log file. --- homeassistant/components/fritz/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index f34cda67ad9b36..afe0aa3ed02423 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -85,6 +85,6 @@ def _update_info(self): if not self.success_init: return False - _LOGGER.info("Scanning") + _LOGGER.debug("Scanning") self.last_results = self.fritz_box.get_hosts_info() return True From d4981a1143aea2a571b557ff3c0e9fe58b3ccfbf Mon Sep 17 00:00:00 2001 From: Robert Dunmire III Date: Sun, 18 Aug 2019 11:14:54 -0400 Subject: [PATCH 063/213] Fix Mikrotik ARP ping (#25965) * Reuse ssl_wraper * Fix arp_ping * Restore debug log * Fix attributes --- homeassistant/components/mikrotik/__init__.py | 14 +++++++++----- homeassistant/components/mikrotik/const.py | 1 + .../components/mikrotik/device_tracker.py | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 236892a98b916c..aacd3c65b3eb64 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from .const import ( + NAME, DOMAIN, HOSTS, MTK_LOGIN_PLAIN, @@ -121,6 +122,7 @@ def __init__(self, host, use_ssl, port, user, password, login_method, encoding): self._password = password self._login_method = login_method self._encoding = encoding + self._ssl_wrapper = None self.hostname = None self._client = None self._connected = False @@ -137,10 +139,12 @@ def connect_to_device(self): } if self._use_ssl: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - kwargs["ssl_wrapper"] = ssl_context.wrap_socket + if self._ssl_wrapper is None: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + self._ssl_wrapper = ssl_context.wrap_socket + kwargs["ssl_wrapper"] = self._ssl_wrapper try: self._client = librouteros.connect( @@ -163,7 +167,7 @@ def connect_to_device(self): def get_hostname(self): """Return device host name.""" data = self.command(MIKROTIK_SERVICES[IDENTITY]) - return data[0]["name"] if data else None + return data[0][NAME] if data else None def connected(self): """Return connected boolean.""" diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index 4f511d6b418eed..bd26b02fe1b924 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -12,6 +12,7 @@ CONF_ENCODING = "encoding" DEFAULT_ENCODING = "utf-8" +NAME = "name" INFO = "info" IDENTITY = "identity" ARP = "arp" diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 47d3fab28ad1d0..6c3fb559cba750 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -132,8 +132,9 @@ def update_device_tracker(self): if self.arp_ping and self.devices_arp: if mac not in self.devices_arp: continue + ip_address = self.devices_arp[mac]["address"] interface = self.devices_arp[mac]["interface"] - if not self.do_arp_ping(mac, interface): + if not self.do_arp_ping(ip_address, interface): continue attrs = {} @@ -148,20 +149,19 @@ def update_device_tracker(self): for attr in ATTR_DEVICE_TRACKER: if attr in device and device[attr] is not None: attrs[slugify(attr)] = device[attr] - attrs["scanner_type"] = self.method attrs["scanner_host"] = self.host attrs["scanner_hostname"] = self.hostname self.device_tracker[mac] = attrs - def do_arp_ping(self, mac, interface): + def do_arp_ping(self, ip_address, interface): """Attempt to arp ping MAC address via interface.""" params = { "arp-ping": "yes", "interval": "100ms", "count": 3, "interface": interface, - "address": mac, + "address": ip_address, } cmd = "/ping" data = self.api.command(cmd, params) From 95059812772a6b4d712d0380ba6de8f55dbe78c3 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 17:20:30 +0200 Subject: [PATCH 064/213] First line should be in imperative mood --- homeassistant/components/atome/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 08ad81de8b40e8..7fe52fd13a8bb8 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,12 +127,12 @@ def state(self): return self._state def load_file(self, filename): - """Loads a file stored with pickle.""" + """A function to load a file stored with pickle.""" with open(filename, "rb") as file: return pickle.load(file) def save_file(self, content, filename): - """Saves a file stored with pickle.""" + """A function to save a file stored with pickle.""" with open(filename, "wb") as file: pickle.dump(content, file) From 1c3424783271d3c86ef75deebe79e85cecffb664 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:05:13 +0200 Subject: [PATCH 065/213] flake8 stupidity... --- homeassistant/components/atome/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 7fe52fd13a8bb8..62ced807dc374c 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,12 +127,12 @@ def state(self): return self._state def load_file(self, filename): - """A function to load a file stored with pickle.""" + """Load filename.""" with open(filename, "rb") as file: return pickle.load(file) def save_file(self, content, filename): - """A function to save a file stored with pickle.""" + """Save content to a file.""" with open(filename, "wb") as file: pickle.dump(content, file) From 62798f4022861dc8f7510bc8ee899ce06dc36650 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:09:25 +0200 Subject: [PATCH 066/213] Flake8 fixes --- homeassistant/components/atome/sensor.py | 34 +++++++++++++----------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 62ced807dc374c..e8a8d104a8edfb 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -72,6 +72,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True +def load_file(filename): + """Load filename.""" + with open(filename, "rb") as file: + return pickle.load(file) + + +def save_file(content, filename): + """Save content to a file.""" + with open(filename, "wb") as file: + pickle.dump(content, file) + + class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" @@ -126,16 +138,6 @@ def state(self): """Return the state of the sensor.""" return self._state - def load_file(self, filename): - """Load filename.""" - with open(filename, "rb") as file: - return pickle.load(file) - - def save_file(self, content, filename): - """Save content to a file.""" - with open(filename, "wb") as file: - pickle.dump(content, file) - # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -160,11 +162,11 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - self.save_file(session_cookie, self._cookie_path) + save_file(session_cookie, self._cookie_path) # store user id - self.save_file(user_id, self._user_id_path) + save_file(user_id, self._user_id_path) # store user ref - self.save_file(user_reference, self._user_reference_path) + save_file(user_reference, self._user_reference_path) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", @@ -176,7 +178,7 @@ def _login(self, username, password): def _get_data(self, url): - cookie = self.load_file(self._cookie_path) + cookie = load_file(self._cookie_path) cookies = {COOKIE_NAME: cookie} req = requests.get(url, cookies=cookies, timeout=self._timeout) @@ -199,8 +201,8 @@ def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = self.load_file(self._user_id_path) - user_reference = self.load_file(self._user_reference_path) + user_id = load_file(self._user_id_path) + user_reference = load_file(self._user_reference_path) url = ( API_BASE_URI From 06de9dc4377cc8b6fd3dbc963ac76daa6c4b161f Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:21:27 +0200 Subject: [PATCH 067/213] Just a test --- homeassistant/components/atome/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index e8a8d104a8edfb..750a129616cfe7 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "atome" From 3463cc9cbdc99ac90e6df21dfa9b507bd63279cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 18 Aug 2019 11:36:23 -0700 Subject: [PATCH 068/213] Make sure config flows extend ConfigFlow base class (#26051) --- homeassistant/components/ipma/config_flow.py | 4 ++-- homeassistant/components/met/config_flow.py | 4 ++-- homeassistant/components/smhi/config_flow.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index c0bc383abc01fa..d1532066f68cd0 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure IPMA component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -9,7 +9,7 @@ @config_entries.HANDLERS.register(DOMAIN) -class IpmaFlowHandler(data_entry_flow.FlowHandler): +class IpmaFlowHandler(config_entries.ConfigFlow): """Config flow for IPMA component.""" VERSION = 1 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 9088d958cf0cad..e903c717e64f46 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure Met component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -18,7 +18,7 @@ def configured_instances(hass): @config_entries.HANDLERS.register(DOMAIN) -class MetFlowHandler(data_entry_flow.FlowHandler): +class MetFlowHandler(config_entries.ConfigFlow): """Config flow for Met component.""" VERSION = 1 diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index a68c8293a9fbc6..3b60cb66165015 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure SMHI component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -21,7 +21,7 @@ def smhi_locations(hass: HomeAssistant): @config_entries.HANDLERS.register(DOMAIN) -class SmhiFlowHandler(data_entry_flow.FlowHandler): +class SmhiFlowHandler(config_entries.ConfigFlow): """Config flow for SMHI component.""" VERSION = 1 From 34f7bb5b8c06d4dd96c36f3f84d09fdb9a48679b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 12:09:56 +0200 Subject: [PATCH 069/213] Revert "Fix bmw_connected_drive and eq3btsmart components by updating their dependencies (#26012)" (#26065) This reverts commit 9e5243929e3821daa44ecb592406fa4a4da88cd1. --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) mode change 100755 => 100644 homeassistant/components/bmw_connected_drive/manifest.json mode change 100755 => 100644 homeassistant/components/eq3btsmart/manifest.json diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json old mode 100755 new mode 100644 index ad5f712f817739..eec81aa65250ee --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,7 +3,7 @@ "name": "Bmw connected drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.6" + "bimmer_connected==0.5.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json old mode 100755 new mode 100644 index 26d732fc9278b0..6d13c79bcec097 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/eq3btsmart", "requirements": [ "construct==2.9.45", - "python-eq3bt==0.1.11" + "python-eq3bt==0.1.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3b00f17f8c25fd..a05fc86ff0fd66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -266,7 +266,7 @@ beautifulsoup4==4.8.0 bellows-homeassistant==0.9.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.6 +bimmer_connected==0.5.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -1443,7 +1443,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.0.21 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.11 +# python-eq3bt==0.1.9 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From 6b80df9652998978ebfe6e710936e6c51b109fe0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 12:15:41 +0200 Subject: [PATCH 070/213] Bump nabucasa-cloud to 0.17 (#26066) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 58739bededc7b3..3daeac43da942c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": ["hass-nabucasa==0.16"], + "requirements": ["hass-nabucasa==0.17"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a898dc497d8a4a..1abb8d1d8224ab 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.16 +hass-nabucasa==0.17 home-assistant-frontend==20190815.0 importlib-metadata==0.19 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index a05fc86ff0fd66..c7b02188df6078 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -597,7 +597,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcdf314727e6ca..3a81c3ef290539 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 From 15ab004e98413ae4e3e75c6329f2dcd289076118 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 19 Aug 2019 14:00:47 +0200 Subject: [PATCH 071/213] fix alarm webhooks (#26062) --- homeassistant/components/point/alarm_control_panel.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 7dc7e164788e99..4a0db111b7d51f 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -17,7 +17,7 @@ EVENT_MAP = { "off": STATE_ALARM_DISARMED, - "alarm_silenced": STATE_ALARM_ARMED_AWAY, + "alarm_silenced": STATE_ALARM_DISARMED, "alarm_grace_period_expired": STATE_ALARM_TRIGGERED, } @@ -63,11 +63,14 @@ def _webhook_event(self, data, webhook): """Process new event from the webhook.""" _type = data.get("event", {}).get("type") _device_id = data.get("event", {}).get("device_id") - if _device_id not in self._home["devices"] or _type not in EVENT_MAP: + _changed_by = data.get("event", {}).get("user_id") + if ( + _device_id not in self._home["devices"] and _type not in EVENT_MAP + ) and _type != "alarm_silenced": # alarm_silenced does not have device_id return _LOGGER.debug("Received webhook: %s", _type) - self._home["alarm_status"] = EVENT_MAP[_type] - self._changed_by = _device_id + self._home["alarm_status"] = _type + self._changed_by = _changed_by self.async_schedule_update_ha_state() @property From 75e18d428252ab2314a00cdd23d6c252e45eac4a Mon Sep 17 00:00:00 2001 From: Philipp Danner Date: Mon, 19 Aug 2019 14:29:26 +0200 Subject: [PATCH 072/213] Add Keba charging station/wallbox as component (#24484) * Add Keba charging station wallbox component * Added start/stop commands (ena 0 and ena 1) * added refresh_interval parameter and fixed authorization * fixed max line length * deactivate failsafe mode if not set in configuration * extracted I/O code to pypi library * updated services.yaml * pinned version of requirements * fixed typos, indent and comments * simplified sensor generation, fixed unique_id and name of sensors * cleaned up data extraction * flake8 fixes * added fast polling, fixed unique_id, code cleanup * updated requirements * fixed pylint * integrated code styling suggestions * fixed pylint * code style changes according to suggestions and pylint fixes * formatted with black * clarefied variables * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare * fixed behaviour if no charging station was found * fix pylint * Update homeassistant/components/keba/__init__.py Co-Authored-By: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/keba/__init__.py | 229 ++++++++++++++++++ .../components/keba/binary_sensor.py | 108 +++++++++ homeassistant/components/keba/lock.py | 69 ++++++ homeassistant/components/keba/manifest.json | 10 + homeassistant/components/keba/sensor.py | 109 +++++++++ homeassistant/components/keba/services.yaml | 56 +++++ requirements_all.txt | 3 + 9 files changed, 586 insertions(+) create mode 100644 homeassistant/components/keba/__init__.py create mode 100644 homeassistant/components/keba/binary_sensor.py create mode 100644 homeassistant/components/keba/lock.py create mode 100644 homeassistant/components/keba/manifest.json create mode 100644 homeassistant/components/keba/sensor.py create mode 100644 homeassistant/components/keba/services.yaml diff --git a/.coveragerc b/.coveragerc index 11b5b91ae22282..d8153a7635c395 100644 --- a/.coveragerc +++ b/.coveragerc @@ -308,6 +308,7 @@ omit = homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* homeassistant/components/kankun/switch.py + homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* diff --git a/CODEOWNERS b/CODEOWNERS index 9c2fa006a13481..f6b9e79b8bd71e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -143,6 +143,7 @@ homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py new file mode 100644 index 00000000000000..5a9a49a005a8dc --- /dev/null +++ b/homeassistant/components/keba/__init__.py @@ -0,0 +1,229 @@ +"""Support for KEBA charging stations.""" +import asyncio +import logging + +from keba_kecontact.connection import KebaKeContact +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "keba" +SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock"] + +CONF_RFID = "rfid" +CONF_FS = "failsafe" +CONF_FS_TIMEOUT = "failsafe_timeout" +CONF_FS_FALLBACK = "failsafe_fallback" +CONF_FS_PERSIST = "failsafe_persist" +CONF_FS_INTERVAL = "refresh_interval" + +MAX_POLLING_INTERVAL = 5 # in seconds +MAX_FAST_POLLING_COUNT = 4 + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_RFID, default="00845500"): cv.string, + vol.Optional(CONF_FS, default=False): cv.boolean, + vol.Optional(CONF_FS_TIMEOUT, default=30): cv.positive_int, + vol.Optional(CONF_FS_FALLBACK, default=6): cv.positive_int, + vol.Optional(CONF_FS_PERSIST, default=0): cv.positive_int, + vol.Optional(CONF_FS_INTERVAL, default=5): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_SERVICE_MAP = { + "request_data": "request_data", + "set_energy": "async_set_energy", + "set_current": "async_set_current", + "authorize": "async_start", + "deauthorize": "async_stop", + "enable": "async_enable_ev", + "disable": "async_disable_ev", + "set_failsafe": "async_set_failsafe", +} + + +async def async_setup(hass, config): + """Check connectivity and version of KEBA charging station.""" + host = config[DOMAIN][CONF_HOST] + rfid = config[DOMAIN][CONF_RFID] + refresh_interval = config[DOMAIN][CONF_FS_INTERVAL] + keba = KebaHandler(hass, host, rfid, refresh_interval) + hass.data[DOMAIN] = keba + + # Wait for KebaHandler setup complete (initial values loaded) + if not await keba.setup(): + _LOGGER.error("Could not find a charging station at %s", host) + return False + + # Set failsafe mode at start up of home assistant + failsafe = config[DOMAIN][CONF_FS] + timeout = config[DOMAIN][CONF_FS_TIMEOUT] if failsafe else 0 + fallback = config[DOMAIN][CONF_FS_FALLBACK] if failsafe else 0 + persist = config[DOMAIN][CONF_FS_PERSIST] if failsafe else 0 + try: + hass.loop.create_task(keba.set_failsafe(timeout, fallback, persist)) + except ValueError as ex: + _LOGGER.warning("Could not set failsafe mode %s", ex) + + # Register services to hass + async def execute_service(call): + """Execute a service to KEBA charging station. + + This must be a member function as we need access to the keba + object here. + """ + function_name = _SERVICE_MAP[call.service] + function_call = getattr(keba, function_name) + await function_call(call.data) + + for service in _SERVICE_MAP: + hass.services.async_register(DOMAIN, service, execute_service) + + # Load components + for domain in SUPPORTED_COMPONENTS: + hass.async_create_task( + discovery.async_load_platform(hass, domain, DOMAIN, {}, config) + ) + + # Start periodic polling of charging station data + keba.start_periodic_request() + + return True + + +class KebaHandler(KebaKeContact): + """Representation of a KEBA charging station connection.""" + + def __init__(self, hass, host, rfid, refresh_interval): + """Constructor.""" + super().__init__(host, self.hass_callback) + + self._update_listeners = [] + self._hass = hass + self.rfid = rfid + self.device_name = "keba_wallbox_" + + # Ensure at least MAX_POLLING_INTERVAL seconds delay + self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) + self._fast_polling_count = MAX_FAST_POLLING_COUNT + self._polling_task = None + + def start_periodic_request(self): + """Start periodic data polling.""" + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def _periodic_request(self): + """Send periodic update requests.""" + await self.request_data() + + if self._fast_polling_count < MAX_FAST_POLLING_COUNT: + self._fast_polling_count += 1 + _LOGGER.debug("Periodic data request executed, now wait for 2 seconds") + await asyncio.sleep(2) + else: + _LOGGER.debug( + "Periodic data request executed, now wait for %s seconds", + self._refresh_interval, + ) + await asyncio.sleep(self._refresh_interval) + + _LOGGER.debug("Periodic data request rescheduled") + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def setup(self, loop=None): + """Initialize KebaHandler object.""" + await super().setup(loop) + + # Request initial values and extract serial number + await self.request_data() + if self.get_value("Serial") is not None: + self.device_name = f"keba_wallbox_{self.get_value('Serial')}" + return True + + return False + + def hass_callback(self, data): + """Handle component notification via callback.""" + + # Inform entities about updated values + for listener in self._update_listeners: + listener() + + _LOGGER.debug("Notifying %d listeners", len(self._update_listeners)) + + def _set_fast_polling(self): + _LOGGER.debug("Fast polling enabled") + self._fast_polling_count = 0 + self._polling_task.cancel() + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + def add_update_listener(self, listener): + """Add a listener for update notifications.""" + self._update_listeners.append(listener) + + # initial data is already loaded, thus update the component + listener() + + async def async_set_energy(self, param): + """Set energy target in async way.""" + try: + energy = param["energy"] + await self.set_energy(energy) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning("Energy value is not correct. %s", ex) + + async def async_set_current(self, param): + """Set current maximum in async way.""" + try: + current = param["current"] + await self.set_current(current) + # No fast polling as this function might be called regularly + except (KeyError, ValueError) as ex: + _LOGGER.warning("Current value is not correct. %s", ex) + + async def async_start(self, param=None): + """Authorize EV in async way.""" + await self.start(self.rfid) + self._set_fast_polling() + + async def async_stop(self, param=None): + """De-authorize EV in async way.""" + await self.stop(self.rfid) + self._set_fast_polling() + + async def async_enable_ev(self, param=None): + """Enable EV in async way.""" + await self.enable(True) + self._set_fast_polling() + + async def async_disable_ev(self, param=None): + """Disable EV in async way.""" + await self.enable(False) + self._set_fast_polling() + + async def async_set_failsafe(self, param=None): + """Set failsafe mode in async way.""" + try: + timout = param[CONF_FS_TIMEOUT] + fallback = param[CONF_FS_FALLBACK] + persist = param[CONF_FS_PERSIST] + await self.set_failsafe(timout, fallback, persist) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning( + "failsafe_timeout, failsafe_fallback and/or " + "failsafe_persist value are not correct. %s", + ex, + ) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py new file mode 100644 index 00000000000000..8c0503a2020f0b --- /dev/null +++ b/homeassistant/components/keba/binary_sensor.py @@ -0,0 +1,108 @@ +"""Support for KEBA charging station binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PLUG, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SAFETY, +) + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [ + KebaBinarySensor(keba, "Online", "Wallbox", DEVICE_CLASS_CONNECTIVITY), + KebaBinarySensor(keba, "Plug", "Plug", DEVICE_CLASS_PLUG), + KebaBinarySensor(keba, "State", "Charging state", DEVICE_CLASS_POWER), + KebaBinarySensor(keba, "Tmo FS", "Failsafe Mode", DEVICE_CLASS_SAFETY), + ] + async_add_entities(sensors) + + +class KebaBinarySensor(BinarySensorDevice): + """Representation of a binary sensor of a KEBA charging station.""" + + def __init__(self, keba, key, sensor_name, device_class): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = sensor_name + self._device_class = device_class + self._is_on = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._is_on + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + if self._key == "Online": + self._is_on = self._keba.get_value(self._key) + + elif self._key == "Plug": + self._is_on = self._keba.get_value("Plug_plugged") + self._attributes["plugged_on_wallbox"] = self._keba.get_value( + "Plug_wallbox" + ) + self._attributes["plug_locked"] = self._keba.get_value("Plug_locked") + self._attributes["plugged_on_EV"] = self._keba.get_value("Plug_EV") + + elif self._key == "State": + self._is_on = self._keba.get_value("State_on") + self._attributes["status"] = self._keba.get_value("State_details") + self._attributes["max_charging_rate"] = str( + self._keba.get_value("Max curr") + ) + + elif self._key == "Tmo FS": + self._is_on = not self._keba.get_value("FS_on") + self._attributes["failsafe_timeout"] = str(self._keba.get_value("Tmo FS")) + self._attributes["fallback_current"] = str(self._keba.get_value("Curr FS")) + elif self._key == "Authreq": + self._is_on = self._keba.get_value(self._key) == 0 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py new file mode 100644 index 00000000000000..3a65e44cd6fcb0 --- /dev/null +++ b/homeassistant/components/keba/lock.py @@ -0,0 +1,69 @@ +"""Support for KEBA charging station switch.""" +import logging + +from homeassistant.components.lock import LockDevice + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [KebaLock(keba, "Authentication")] + async_add_entities(sensors) + + +class KebaLock(LockDevice): + """The entity class for KEBA charging stations switch.""" + + def __init__(self, keba, name): + """Initialize the KEBA switch.""" + self._keba = keba + self._name = name + self._state = True + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state + + async def async_lock(self, **kwargs): + """Lock wallbox.""" + await self._keba.async_stop() + + async def async_unlock(self, **kwargs): + """Unlock wallbox.""" + await self._keba.async_start() + + async def async_update(self): + """Attempt to retrieve on off state from the switch.""" + self._state = self._keba.get_value("Authreq") == 1 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json new file mode 100644 index 00000000000000..9e959f35c9f32a --- /dev/null +++ b/homeassistant/components/keba/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "keba", + "name": "Keba Charging Station", + "documentation": "https://www.home-assistant.io/components/keba", + "requirements": ["keba-kecontact==0.2.0"], + "dependencies": [], + "codeowners": [ + "@dannerph" + ] +} diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py new file mode 100644 index 00000000000000..f46b2f0cf9023d --- /dev/null +++ b/homeassistant/components/keba/sensor.py @@ -0,0 +1,109 @@ +"""Support for KEBA charging station sensors.""" +import logging + +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.helpers.entity import Entity +from homeassistant.const import DEVICE_CLASS_POWER + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [ + KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor( + keba, "Setenergy", "Energy target", "mdi:gauge", ENERGY_KILO_WATT_HOUR + ), + KebaSensor(keba, "P", "Charging power", "mdi:flash", "kW", DEVICE_CLASS_POWER), + KebaSensor( + keba, "E pres", "Session energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR + ), + KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), + ] + async_add_entities(sensors) + + +class KebaSensor(Entity): + """The entity class for KEBA charging stations sensors.""" + + def __init__(self, keba, key, name, icon, unit, device_class=None): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = name + self._device_class = device_class + self._icon = icon + self._unit = unit + self._state = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + self._state = self._keba.get_value(self._key) + + if self._key == "P": + self._attributes["power_factor"] = self._keba.get_value("PF") + self._attributes["voltage_u1"] = str(self._keba.get_value("U1")) + self._attributes["voltage_u2"] = str(self._keba.get_value("U2")) + self._attributes["voltage_u3"] = str(self._keba.get_value("U3")) + self._attributes["current_i1"] = str(self._keba.get_value("I1")) + self._attributes["current_i2"] = str(self._keba.get_value("I2")) + self._attributes["current_i3"] = str(self._keba.get_value("I3")) + elif self._key == "Curr user": + self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml new file mode 100644 index 00000000000000..3422d6cf034d58 --- /dev/null +++ b/homeassistant/components/keba/services.yaml @@ -0,0 +1,56 @@ +# Describes the format for available services for KEBA charging staitons + +request_data: + description: > + Request new data from the charging station. + +authorize: + description: > + Authorizes a charging process with the predefined RFID tag of the configuration file. + +deauthorize: + description: > + Deauthorizes the running charging process with the predefined RFID tag of the configuration file. + +set_energy: + description: Sets the energy target after which the charging process stops. + fields: + energy: + description: > + The energy target to stop charging in kWh. Setting 0 disables the limit. + example: 10.0 + +set_current: + description: Sets the maximum current for charging processes. + fields: + current: + description: > + The maximum current used for the charging process in A. Allowed are values between + 6 A and 63 A. Invalid values are discardedand the default is set to 6 A. + The value is also depending on the DIP-switchsettings and the used cable of the + charging station + example: 16 +enable: + description: > + Starts a charging process if charging station is authorized. + +disable: + description: > + Stops the charging process if charging station is authorized. + +set_failsafe: + description: > + Set the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled. + fields: + failsafe_timeout: + description: > + Timeout in seconds after which the failsafe mode is triggered, if set_current was not executed during this time. + example: 30 + failsafe_fallback: + description: > + Fallback current in A to be set after timeout. + example: 6 + failsafe_persist: + description: > + If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. + example: 0 diff --git a/requirements_all.txt b/requirements_all.txt index c7b02188df6078..cdf3a781883c01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -692,6 +692,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.keba +keba-kecontact==0.2.0 + # homeassistant.scripts.keyring keyring==17.1.1 From 1077ec17043e8364213e16bfc5bb6187d7590943 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 19 Aug 2019 16:56:57 +0200 Subject: [PATCH 073/213] Add packages version to Tensoflow for wheels packages (#26068) * Add packages version to Tensoflow for wheels packages * Uncomment tensorflow * fix string * Revert version --- homeassistant/components/tensorflow/manifest.json | 3 ++- requirements_all.txt | 3 +++ script/gen_requirements_all.py | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9cbd349addcb73..f5bd981bad1dc4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,10 +3,11 @@ "name": "Tensorflow", "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ + "tensorflow==1.13.2", "numpy==1.17.0", "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index cdf3a781883c01..5017e01fb95785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1820,6 +1820,9 @@ temescal==0.1 # homeassistant.components.temper temperusb==1.5.3 +# homeassistant.components.tensorflow +# tensorflow==1.13.2 + # homeassistant.components.tesla teslajsonpy==0.0.25 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9417b9264239ff..909be48352d1f7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -38,6 +38,7 @@ "rpi-rf", "RPi.GPIO", "smbus-cffi", + "tensorflow", ) TEST_REQUIREMENTS = ( From a38bdc4deb825fa673bad8114583ca20e46d59e5 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Mon, 19 Aug 2019 10:08:07 -0700 Subject: [PATCH 074/213] Remove reference to typing.Deque (added in Python 3.6.1) (#26030) * Remove reference to typing.Deque (added in Python 3.6.1) * Silence mypy * Type as collections.deque --- homeassistant/components/camera/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1b2bfb5fdb1363..597d67fcdeec63 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -7,7 +7,6 @@ import logging import hashlib from random import SystemRandom -from typing import Deque import attr from aiohttp import web @@ -315,7 +314,7 @@ def __init__(self): """Initialize a camera.""" self.is_streaming = False self.content_type = DEFAULT_CONTENT_TYPE - self.access_tokens: Deque[str] = collections.deque([], 2) + self.access_tokens: collections.deque = collections.deque([], 2) self.async_update_token() @property From 45aec2ea40c45ce74f87e0988e7659cfc316abc4 Mon Sep 17 00:00:00 2001 From: flebourse Date: Mon, 19 Aug 2019 19:34:33 +0200 Subject: [PATCH 075/213] huawei_lte: support out of range values in default sensor formatter (#26052) * Update sensor.py Change regexp to cope with out of range values, ie "rssi" : ">=-51dBm". * Add tests for format_default * Fix black formatting --- homeassistant/components/huawei_lte/sensor.py | 4 +++- tests/components/huawei_lte/test_sensor.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/components/huawei_lte/test_sensor.py diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 4ef88eb783e7b1..da78dc7d8cf7a7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -137,7 +137,9 @@ def format_default(value): unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match(r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) + match = re.match( + r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) + ) if match: try: value = float(match.group("value")) diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py new file mode 100644 index 00000000000000..f9834349750339 --- /dev/null +++ b/tests/components/huawei_lte/test_sensor.py @@ -0,0 +1,14 @@ +"""Huawei LTE sensor tests.""" + +import pytest + +from homeassistant.components.huawei_lte import sensor + + +@pytest.mark.parametrize( + ("value", "expected"), + (("-71 dBm", (-71, "dBm")), ("15dB", (15, "dB")), (">=-51dBm", (-51, "dBm"))), +) +def test_format_default(value, expected): + """Test that default formatter copes with expected values.""" + assert sensor.format_default(value) == expected From b867e3314b670acd90826c2a27cf844b6495fbec Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Aug 2019 22:10:35 +0200 Subject: [PATCH 076/213] SMA simplify config (#25880) --- homeassistant/components/sma/sensor.py | 85 ++++++++++++++++++-------- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sma/__init__.py | 1 + tests/components/sma/test_sensor.py | 51 ++++++++++++++++ 5 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 tests/components/sma/__init__.py create mode 100644 tests/components/sma/test_sensor.py diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 8e6b94ef5f8b42..b2692a37059be0 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -19,6 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import MINOR_VERSION, MAJOR_VERSION _LOGGER = logging.getLogger(__name__) @@ -30,6 +31,7 @@ CONF_UNIT = "unit" GROUPS = ["user", "installer"] +OLD_CONFIG_DEPRECATED = MAJOR_VERSION > 0 or MINOR_VERSION > 98 def _check_sensor_schema(conf): @@ -41,16 +43,39 @@ def _check_sensor_schema(conf): except (ImportError, AttributeError): return conf - for name in conf[CONF_CUSTOM]: - valid.append(name) + customs = list(conf[CONF_CUSTOM].keys()) + + if isinstance(conf[CONF_SENSORS], dict): + msg = '"sensors" should be a simple list from 0.99' + if OLD_CONFIG_DEPRECATED: + raise vol.Invalid(msg) + _LOGGER.warning(msg) + valid.extend(customs) + + for sname, attrs in conf[CONF_SENSORS].items(): + if sname not in valid: + raise vol.Invalid("{} does not exist".format(sname)) + if attrs: + _LOGGER.warning( + "Attributes on sensors will be deprecated in 0.99. Start using only individual sensors: %s: %s", + sname, + ", ".join(attrs), + ) + for attr in attrs: + if attr in valid: + continue + raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + return conf - for sname, attrs in conf[CONF_SENSORS].items(): - if sname not in valid: - raise vol.Invalid("{} does not exist".format(sname)) - for attr in attrs: - if attr in valid: - continue - raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + # Sensors is a list (only option from from 0.99) + for sensor in conf[CONF_SENSORS]: + if sensor in customs: + _LOGGER.warning( + "All custom sensors will be added automatically, no need to include them in sensors: %s", + sensor, + ) + elif sensor not in valid: + raise vol.Invalid("{} does not exist".format(sensor)) return conf @@ -59,7 +84,7 @@ def _check_sensor_schema(conf): vol.Required(CONF_KEY): vol.All(cv.string, vol.Length(min=13, max=15)), vol.Required(CONF_UNIT): cv.string, vol.Optional(CONF_FACTOR, default=1): vol.Coerce(float), - vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [cv.string]), } ) @@ -71,8 +96,9 @@ def _check_sensor_schema(conf): vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), - vol.Optional(CONF_SENSORS, default={}): cv.schema_with_slug_keys( - cv.ensure_list + vol.Optional(CONF_SENSORS, default=[]): vol.Any( + cv.schema_with_slug_keys(cv.ensure_list), # will be deprecated + vol.All(cv.ensure_list, [str]), ), vol.Optional(CONF_CUSTOM, default={}): cv.schema_with_slug_keys( CUSTOM_SCHEMA @@ -104,20 +130,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default config_sensors = config[CONF_SENSORS] - if not config_sensors: - config_sensors = {s.name: [] for s in sensor_def} - - # Prepare all HASS sensor entities hass_sensors = [] used_sensors = [] - for name, attr in config_sensors.items(): - sub_sensors = [sensor_def[s] for s in attr] - hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) - used_sensors.append(name) - used_sensors.extend(attr) + + if isinstance(config_sensors, dict): # will be remove from 0.99 + if not config_sensors: # Use all sensors by default + config_sensors = {s.name: [] for s in sensor_def} + + # Prepare all HASS sensor entities + for name, attr in config_sensors.items(): + sub_sensors = [sensor_def[s] for s in attr] + hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) + used_sensors.append(name) + used_sensors.extend(attr) + used_sensors = [sensor_def[s] for s in set(used_sensors)] + + if isinstance(config_sensors, list): + if not config_sensors: # Use all sensors by default + config_sensors = [s.name for s in sensor_def] + used_sensors = list(set(config_sensors + list(config[CONF_CUSTOM].keys()))) + for sensor in used_sensors: + hass_sensors.append(SMAsensor(sensor_def[sensor], [])) async_add_entities(hass_sensors) - used_sensors = [sensor_def[s] for s in set(used_sensors)] # Init the SMA interface session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL]) @@ -172,7 +207,7 @@ class SMAsensor(Entity): def __init__(self, pysma_sensor, sub_sensors): """Initialize the sensor.""" self._sensor = pysma_sensor - self._sub_sensors = sub_sensors + self._sub_sensors = sub_sensors # Can be remove from 0.99 self._attr = {s.name: "" for s in sub_sensors} self._state = self._sensor.value @@ -193,7 +228,7 @@ def unit_of_measurement(self): return self._sensor.unit @property - def device_state_attributes(self): + def device_state_attributes(self): # Can be remove from 0.99 """Return the state attributes of the sensor.""" return self._attr @@ -206,7 +241,7 @@ def async_update_values(self): """Update this sensor.""" update = False - for sens in self._sub_sensors: + for sens in self._sub_sensors: # Can be remove from 0.99 newval = "{} {}".format(sens.value, sens.unit) if self._attr[sens.name] != newval: update = True diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a81c3ef290539..a091027db741fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,6 +309,9 @@ pyps4-homeassistant==0.8.7 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.sma +pysma==0.3.2 + # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 909be48352d1f7..bcf645034f53b3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -123,6 +123,7 @@ "pyopenuv", "pyotp", "pyps4-homeassistant", + "pysma", "pysmartapp", "pysmartthings", "pysonos", diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py new file mode 100644 index 00000000000000..124f481135ea97 --- /dev/null +++ b/tests/components/sma/__init__.py @@ -0,0 +1 @@ +"""SMA tests.""" diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py new file mode 100644 index 00000000000000..bee1743791c597 --- /dev/null +++ b/tests/components/sma/test_sensor.py @@ -0,0 +1,51 @@ +"""SMA sensor tests.""" +import logging + +from homeassistant.components.sensor import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +_LOGGER = logging.getLogger(__name__) +BASE_CFG = { + "platform": "sma", + "host": "1.1.1.1", + "password": "", + "custom": {"my_sensor": {"key": "1234567890123", "unit": "V"}}, +} + + +async def test_sma_config_old(hass): + """Test old config.""" + sensors = {"current_consumption": ["current_consumption"]} + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert not state + + +async def test_sma_config(hass): + """Test new config.""" + sensors = ["current_consumption"] + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" not in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert state From e41c002f7091bcd0e8f4c22ed9b224e896a0ef2e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 19 Aug 2019 23:42:21 +0200 Subject: [PATCH 077/213] Add config entry options support to deCONZ (#26049) --- .../components/deconz/.translations/en.json | 13 ++++- .../components/deconz/config_flow.py | 53 ++++++++++++++++++- homeassistant/components/deconz/strings.json | 11 ++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index dd8f1cc4026edb..34da602a6cee52 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index f4b8d3ebe0262d..650c02857509da 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure deCONZ component.""" import asyncio +from copy import copy import async_timeout import voluptuous as vol @@ -12,7 +13,13 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN +from .const import ( + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_BRIDGEID, + DEFAULT_PORT, + DOMAIN, +) DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" @@ -45,6 +52,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): _hassio_discovery = None + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DeconzOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the deCONZ config flow.""" self.bridges = [] @@ -234,3 +247,41 @@ async def async_step_hassio_confirm(self, user_input=None): step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, ) + + +class DeconzOptionsFlowHandler(config_entries.OptionsFlow): + """Handle deCONZ options.""" + + def __init__(self, config_entry): + """Initialize deCONZ options flow.""" + self.config_entry = config_entry + self.options = copy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the deCONZ options.""" + return await self.async_step_deconz_devices() + + async def async_step_deconz_devices(self, user_input=None): + """Manage the deconz devices options.""" + if user_input is not None: + self.options[CONF_ALLOW_CLIP_SENSOR] = user_input[CONF_ALLOW_CLIP_SENSOR] + self.options[CONF_ALLOW_DECONZ_GROUPS] = user_input[ + CONF_ALLOW_DECONZ_GROUPS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="deconz_devices", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_CLIP_SENSOR, + default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR], + ): bool, + vol.Optional( + CONF_ALLOW_DECONZ_GROUPS, + default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS], + ): bool, + } + ), + ) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index d1c70793063ee6..ea9ea2805155c6 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -40,5 +40,16 @@ "one_instance_only": "Component only supports one deCONZ instance", "updated_instance": "Updated deCONZ instance with new host address" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } } } From d1483b6f29501159c780ffb74aa415c31cb1b810 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 20 Aug 2019 01:44:19 +0200 Subject: [PATCH 078/213] pysma library update 0.3.4 (#26075) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 8795029bff2e62..ea3a33d55ff818 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "Sma", "documentation": "https://www.home-assistant.io/components/sma", "requirements": [ - "pysma==0.3.2" + "pysma==0.3.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5017e01fb95785..8e9cf5d71470f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sma -pysma==0.3.2 +pysma==0.3.4 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a091027db741fc..6d8feb7b984ac4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -310,7 +310,7 @@ pyps4-homeassistant==0.8.7 pyqwikswitch==0.93 # homeassistant.components.sma -pysma==0.3.2 +pysma==0.3.4 # homeassistant.components.smartthings pysmartapp==0.3.2 From a1dbdbba6a2647c96e44e17a0192c7a3210a9323 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 Aug 2019 16:45:17 -0700 Subject: [PATCH 079/213] Save config entry after updating system options (#26077) --- homeassistant/components/config/config_entries.py | 10 +++++++--- homeassistant/config_entries.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d7c8a6ea8e08c5..90ae92cae84afd 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -272,7 +272,11 @@ async def system_options_update(hass, connection, msg): entry_id = changes.pop("entry_id") entry = hass.config_entries.async_get_entry(entry_id) - if entry and changes: - entry.system_options.update(**changes) + if entry is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return - connection.send_result(msg["id"], entry.system_options.as_dict()) + hass.config_entries.async_update_entry(entry, system_options=changes) + connection.send_result(msg["id"], entry.system_options.as_dict()) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9844aeb9ca679f..87bce1a870c249 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -522,7 +522,9 @@ async def async_reload(self, entry_id: str) -> bool: return await self.async_setup(entry_id) @callback - def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): + def async_update_entry( + self, entry, *, data=_UNDEF, options=_UNDEF, system_options=_UNDEF + ): """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -530,10 +532,12 @@ def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): if options is not _UNDEF: entry.options = options - if data is not _UNDEF or options is not _UNDEF: - for listener_ref in entry.update_listeners: - listener = listener_ref() - self.hass.async_create_task(listener(self.hass, entry)) + if system_options is not _UNDEF: + entry.system_options.update(**system_options) + + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() From 1fe3b147fa2993b6aee763e0fd98f22c431ae2ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 00:15:53 -0700 Subject: [PATCH 080/213] Updated frontend to 20190820.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2ccda8a1f4a1fe..f7c1a4f874f096 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190815.0" + "home-assistant-frontend==20190820.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1abb8d1d8224ab..74ffb83f884514 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8e9cf5d71470f4..9c578e45b1c2d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -627,7 +627,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d8feb7b984ac4..7b757d8e1c117b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190815.0 +home-assistant-frontend==20190820.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 93a800a6121f32eb2af9b64e93b1fd0117c81785 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 00:17:52 -0700 Subject: [PATCH 081/213] Convert progress API to WS (#26082) --- .../components/config/config_entries.py | 39 +++++++++++-------- .../components/config/test_config_entries.py | 36 ++++++++++------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 90ae92cae84afd..b21991a8479180 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,5 @@ """Http views to control the config manager.""" +import aiohttp.web_exceptions import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -28,6 +29,7 @@ async def async_setup(hass): OptionManagerFlowResourceView(hass.config_entries.options.flow) ) + hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) @@ -116,23 +118,8 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): name = "api:config:config_entries:flow" async def get(self, request): - """List flows that are in progress but not started by a user. - - Example of a non-user initiated flow is a discovered Hue hub that - requires user interaction to finish setup. - """ - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - - hass = request.app["hass"] - - return self.json( - [ - flw - for flw in hass.config_entries.flow.async_progress() - if flw["context"]["source"] != config_entries.SOURCE_USER - ] - ) + """Not implemented.""" + raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) # pylint: disable=arguments-differ async def post(self, request): @@ -241,6 +228,24 @@ async def post(self, request, flow_id): return await super().post(request, flow_id) +@websocket_api.require_admin +@websocket_api.websocket_command({"type": "config_entries/flow/progress"}) +def config_entries_progress(hass, connection, msg): + """List flows that are in progress but not started by a user. + + Example of a non-user initiated flow is a discovered Hue hub that + requires user interaction to finish setup. + """ + connection.send_result( + msg["id"], + [ + flw + for flw in hass.config_entries.flow.async_progress() + if flw["context"]["source"] != config_entries.SOURCE_USER + ], + ) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f0815e7ede8104..3d22d3ac1a7773 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -373,40 +373,46 @@ def async_step_account(self, user_input=None): assert resp.status == 401 -@asyncio.coroutine -def test_get_progress_index(hass, client): +async def test_get_progress_index(hass, hass_ws_client): """Test querying for the flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) mock_entity_platform(hass, "config_flow.test", None) + ws_client = await hass_ws_client(hass) class TestFlow(core_ce.ConfigFlow): VERSION = 5 - @asyncio.coroutine - def async_step_hassio(self, info): - return (yield from self.async_step_account()) + async def async_step_hassio(self, info): + return await self.async_step_account() - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_show_form(step_id="account") with patch.dict(HANDLERS, {"test": TestFlow}): - form = yield from hass.config_entries.flow.async_init( + form = await hass.config_entries.flow.async_init( "test", context={"source": "hassio"} ) - resp = yield from client.get("/api/config/config_entries/flow") - assert resp.status == 200 - data = yield from resp.json() - assert data == [ + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == [ {"flow_id": form["flow_id"], "handler": "test", "context": {"source": "hassio"}} ] -async def test_get_progress_index_unauth(hass, client, hass_admin_user): +async def test_get_progress_index_unauth(hass, hass_ws_client, hass_admin_user): """Test we can't get flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) hass_admin_user.groups = [] - resp = await client.get("/api/config/config_entries/flow") - assert resp.status == 401 + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unauthorized" @asyncio.coroutine From eee2b2d54346fb83026ac3b590981edd06a60f5d Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Tue, 20 Aug 2019 11:56:11 +0200 Subject: [PATCH 082/213] Add Minio component (#23567) * Add minio implementation * Static check changes * Added docstrings * Update docstrings * Update docstrings * Fix linter errors * Finally fix all docstring errors * Create services.yaml * Update CODEOWNERS * Final changes * Remove double underscores * Minor changes * Update config.yml * Review changes * Added tests * Fix lint errors * Move tests from unittest to pytest * Add minio as test requirement * Update test_minio_helper.py * Better event thread handling, added hass test * Update tests * Fixed lint errors * Update test_minio.py * Review changes * More review changes * Removed tests * Applied code style changes * Reformat test code --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/minio/__init__.py | 265 ++++++++++++++++++ homeassistant/components/minio/manifest.json | 12 + .../components/minio/minio_helper.py | 209 ++++++++++++++ homeassistant/components/minio/services.yaml | 35 +++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/minio/__init__.py | 1 + tests/components/minio/common.py | 48 ++++ tests/components/minio/test_minio.py | 190 +++++++++++++ 12 files changed, 769 insertions(+) create mode 100644 homeassistant/components/minio/__init__.py create mode 100644 homeassistant/components/minio/manifest.json create mode 100644 homeassistant/components/minio/minio_helper.py create mode 100644 homeassistant/components/minio/services.yaml create mode 100644 tests/components/minio/__init__.py create mode 100644 tests/components/minio/common.py create mode 100644 tests/components/minio/test_minio.py diff --git a/.coveragerc b/.coveragerc index d8153a7635c395..e0bbbd66d7cf9b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -378,6 +378,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mobile_app/* diff --git a/CODEOWNERS b/CODEOWNERS index f6b9e79b8bd71e..1425c4764787e2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py new file mode 100644 index 00000000000000..cede3a7aad53c2 --- /dev/null +++ b/homeassistant/components/minio/__init__.py @@ -0,0 +1,265 @@ +"""Minio component.""" +import logging +import os +import threading +from queue import Queue +from typing import List + +import voluptuous as vol + +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv + +from .minio_helper import create_minio_client, MinioEventThread + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "minio" +CONF_HOST = "host" +CONF_PORT = "port" +CONF_ACCESS_KEY = "access_key" +CONF_SECRET_KEY = "secret_key" +CONF_SECURE = "secure" +CONF_LISTEN = "listen" +CONF_LISTEN_BUCKET = "bucket" +CONF_LISTEN_PREFIX = "prefix" +CONF_LISTEN_SUFFIX = "suffix" +CONF_LISTEN_EVENTS = "events" + +ATTR_BUCKET = "bucket" +ATTR_KEY = "key" +ATTR_FILE_PATH = "file_path" + +DEFAULT_LISTEN_PREFIX = "" +DEFAULT_LISTEN_SUFFIX = ".*" +DEFAULT_LISTEN_EVENTS = "s3:ObjectCreated:*" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_ACCESS_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Required(CONF_SECURE): cv.boolean, + vol.Optional(CONF_LISTEN, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_LISTEN_BUCKET): cv.string, + vol.Optional( + CONF_LISTEN_PREFIX, default=DEFAULT_LISTEN_PREFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_SUFFIX, default=DEFAULT_LISTEN_SUFFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_EVENTS, default=DEFAULT_LISTEN_EVENTS + ): cv.string, + } + ) + ], + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +BUCKET_KEY_SCHEMA = vol.Schema( + {vol.Required(ATTR_BUCKET): cv.template, vol.Required(ATTR_KEY): cv.template} +) + +BUCKET_KEY_FILE_SCHEMA = BUCKET_KEY_SCHEMA.extend( + {vol.Required(ATTR_FILE_PATH): cv.template} +) + + +def setup(hass, config): + """Set up MinioClient and event listeners.""" + conf = config[DOMAIN] + + host = conf[CONF_HOST] + port = conf[CONF_PORT] + access_key = conf[CONF_ACCESS_KEY] + secret_key = conf[CONF_SECRET_KEY] + secure = conf[CONF_SECURE] + + queue_listener = QueueListener(hass) + queue = queue_listener.queue + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, queue_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, queue_listener.stop_handler) + + def _setup_listener(listener_conf): + bucket = listener_conf[CONF_LISTEN_BUCKET] + prefix = listener_conf[CONF_LISTEN_PREFIX] + suffix = listener_conf[CONF_LISTEN_SUFFIX] + events = listener_conf[CONF_LISTEN_EVENTS] + + minio_listener = MinioListener( + queue, + get_minio_endpoint(host, port), + access_key, + secret_key, + secure, + bucket, + prefix, + suffix, + events, + ) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, minio_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, minio_listener.stop_handler) + + for listen_conf in conf[CONF_LISTEN]: + _setup_listener(listen_conf) + + minio_client = create_minio_client( + get_minio_endpoint(host, port), access_key, secret_key, secure + ) + + def _render_service_value(service, key): + value = service.data[key] + value.hass = hass + return value.async_render() + + def put_file(service): + """Upload file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fput_object(bucket, key, file_path) + + def get_file(service): + """Download file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fget_object(bucket, key, file_path) + + def remove_file(service): + """Delete file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + + minio_client.remove_object(bucket, key) + + hass.services.register(DOMAIN, "put", put_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "get", get_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "remove", remove_file, schema=BUCKET_KEY_SCHEMA) + + return True + + +def get_minio_endpoint(host: str, port: int) -> str: + """Create minio endpoint from host and port.""" + return "{}:{}".format(host, port) + + +class QueueListener(threading.Thread): + """Forward events from queue into HASS event bus.""" + + def __init__(self, hass): + """Create queue.""" + super().__init__() + self._hass = hass + self._queue = Queue() + + def run(self): + """Listen to queue events, and forward them to HASS event bus.""" + _LOGGER.info("Running QueueListener") + while True: + event = self._queue.get() + if event is None: + break + + _, file_name = os.path.split(event[ATTR_KEY]) + + _LOGGER.debug( + "Sending event %s, %s, %s", + event["event_name"], + event[ATTR_BUCKET], + event[ATTR_KEY], + ) + self._hass.bus.fire(DOMAIN, {"file_name": file_name, **event}) + + @property + def queue(self): + """Return wrapped queue.""" + return self._queue + + def stop(self): + """Stop run by putting None into queue and join the thread.""" + _LOGGER.info("Stopping QueueListener") + self._queue.put(None) + self.join() + _LOGGER.info("Stopped QueueListener") + + def start_handler(self, _): + """Start handler helper method.""" + self.start() + + def stop_handler(self, _): + """Stop handler helper method.""" + self.stop() + + +class MinioListener: + """MinioEventThread wrapper with helper methods.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Create Listener.""" + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._minio_event_thread = None + + def start_handler(self, _): + """Create and start the event thread.""" + self._minio_event_thread = MinioEventThread( + self._queue, + self._endpoint, + self._access_key, + self._secret_key, + self._secure, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + self._minio_event_thread.start() + + def stop_handler(self, _): + """Issue stop and wait for thread to join.""" + if self._minio_event_thread is not None: + self._minio_event_thread.stop() diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json new file mode 100644 index 00000000000000..2b2f84836ead39 --- /dev/null +++ b/homeassistant/components/minio/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "minio", + "name": "Minio", + "documentation": "https://www.home-assistant.io/components/minio", + "requirements": [ + "minio==4.0.9" + ], + "dependencies": [], + "codeowners": [ + "@tkislan" + ] +} diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py new file mode 100644 index 00000000000000..bd7b15d27d4f58 --- /dev/null +++ b/homeassistant/components/minio/minio_helper.py @@ -0,0 +1,209 @@ +"""Minio helper methods.""" +import time +from collections.abc import Iterable +import json +import logging +import re +import threading +from queue import Queue +from typing import Iterator, List +from urllib.parse import unquote + +from minio import Minio +from urllib3.exceptions import HTTPError + +_LOGGER = logging.getLogger(__name__) + +_METADATA_RE = re.compile("x-amz-meta-(.*)", re.IGNORECASE) + + +def normalize_metadata(metadata: dict) -> dict: + """Normalize object metadata by stripping the prefix.""" + new_metadata = {} + for meta_key, meta_value in metadata.items(): + match = _METADATA_RE.match(meta_key) + if not match: + continue + + new_metadata[match.group(1).lower()] = meta_value + + return new_metadata + + +def create_minio_client( + endpoint: str, access_key: str, secret_key: str, secure: bool +) -> Minio: + """Create Minio client.""" + return Minio(endpoint, access_key, secret_key, secure) + + +def get_minio_notification_response( + minio_client, bucket_name: str, prefix: str, suffix: str, events: List[str] +): + """Start listening to minio events. Copied from minio-py.""" + query = {"prefix": prefix, "suffix": suffix, "events": events} + # pylint: disable=protected-access + return minio_client._url_open( + "GET", bucket_name=bucket_name, query=query, preload_content=False + ) + + +class MinioEventStreamIterator(Iterable): + """Iterator wrapper over notification http response stream.""" + + def __iter__(self) -> Iterator: + """Return self.""" + return self + + def __init__(self, response): + """Init.""" + self._response = response + self._stream = response.stream() + + def __next__(self): + """Get next not empty line.""" + while True: + line = next(self._stream) + if line.strip(): + event = json.loads(line.decode("utf-8")) + if event["Records"] is not None: + return event + + def close(self): + """Close the response.""" + self._response.close() + + +class MinioEventThread(threading.Thread): + """Thread wrapper around minio notification blocking stream.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Copy over all Minio client options.""" + super().__init__() + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._event_stream_it = None + self._should_stop = False + + def __enter__(self): + """Start the thread.""" + self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + """Stop and join the thread.""" + self.stop() + + def run(self): + """Create MinioClient and run the loop.""" + _LOGGER.info("Running MinioEventThread") + + self._should_stop = False + + minio_client = create_minio_client( + self._endpoint, self._access_key, self._secret_key, self._secure + ) + + while not self._should_stop: + _LOGGER.info("Connecting to minio event stream") + response = None + try: + response = get_minio_notification_response( + minio_client, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + + self._event_stream_it = MinioEventStreamIterator(response) + + self._iterate_event_stream(self._event_stream_it, minio_client) + except json.JSONDecodeError: + if response: + response.close() + except HTTPError as error: + _LOGGER.error("Failed to connect to Minio endpoint: %s", error) + + # Wait before attempting to connect again. + time.sleep(1) + except AttributeError: + # When response is closed, iterator will fail to access + # the underlying socket descriptor. + break + + def _iterate_event_stream(self, event_stream_it, minio_client): + for event in event_stream_it: + for event_name, bucket, key, metadata in iterate_objects(event): + presigned_url = "" + try: + presigned_url = minio_client.presigned_get_object(bucket, key) + # Fail gracefully. If for whatever reason this stops working, + # it shouldn't prevent it from firing events. + # pylint: disable=broad-except + except Exception as error: + _LOGGER.error("Failed to generate presigned url: %s", error) + + queue_entry = { + "event_name": event_name, + "bucket": bucket, + "key": key, + "presigned_url": presigned_url, + "metadata": metadata, + } + _LOGGER.debug("Queue entry, %s", queue_entry) + self._queue.put(queue_entry) + + def stop(self): + """Cancel event stream and join the thread.""" + _LOGGER.debug("Stopping event thread") + self._should_stop = True + if self._event_stream_it is not None: + self._event_stream_it.close() + self._event_stream_it = None + + _LOGGER.debug("Joining event thread") + self.join() + _LOGGER.debug("Event thread joined") + + +def iterate_objects(event): + """ + Iterate over file records of notification event. + + Most of the time it should still be only one record. + """ + records = event.get("Records", []) + + for record in records: + event_name = record.get("eventName") + bucket = record.get("s3", {}).get("bucket", {}).get("name") + key = record.get("s3", {}).get("object", {}).get("key") + metadata = normalize_metadata( + record.get("s3", {}).get("object", {}).get("userMetadata", {}) + ) + + if not bucket or not key: + _LOGGER.warning("Invalid bucket and/or key, %s, %s", bucket, key) + continue + + key = unquote(key) + + yield event_name, bucket, key, metadata diff --git a/homeassistant/components/minio/services.yaml b/homeassistant/components/minio/services.yaml new file mode 100644 index 00000000000000..8fb8a267c3bd3b --- /dev/null +++ b/homeassistant/components/minio/services.yaml @@ -0,0 +1,35 @@ +get: + description: Download file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +put: + description: Upload file to Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +remove: + description: Delete file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg diff --git a/requirements_all.txt b/requirements_all.txt index 9c578e45b1c2d5..6cd7f5d28224e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -797,6 +797,9 @@ miflora==0.4.0 # homeassistant.components.mill millheater==0.3.4 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.mitemp_bt mitemp_bt==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b757d8e1c117b..e2829d2018c674 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,6 +213,9 @@ mbddns==0.1.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bcf645034f53b3..6643fcf7aa9a35 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -99,6 +99,7 @@ "pyMetno", "mbddns", "mficlient", + "minio", "netdisco", "numpy", "oauth2client", diff --git a/tests/components/minio/__init__.py b/tests/components/minio/__init__.py new file mode 100644 index 00000000000000..273de09788e8d3 --- /dev/null +++ b/tests/components/minio/__init__.py @@ -0,0 +1 @@ +"""Tests for the minio component.""" diff --git a/tests/components/minio/common.py b/tests/components/minio/common.py new file mode 100644 index 00000000000000..4719fc79e49fb4 --- /dev/null +++ b/tests/components/minio/common.py @@ -0,0 +1,48 @@ +"""Minio Test event.""" +TEST_EVENT = { + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "minio:s3", + "awsRegion": "", + "eventTime": "2019-05-02T11:05:07Z", + "eventName": "s3:ObjectCreated:Put", + "userIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "requestParameters": { + "accessKey": "SO9KNO6YT9OGE39PQCZW", + "region": "", + "sourceIPAddress": "172.27.0.1", + }, + "responseElements": { + "x-amz-request-id": "159AD8E6F6805783", + "x-minio-deployment-id": "90b265b8-bac5-413a-b12a-8915469fd769", + "x-minio-origin-endpoint": "http://172.27.0.2:9000", + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "Config", + "bucket": { + "name": "test", + "ownerIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "arn": "arn:aws:s3:::test", + }, + "object": { + "key": "5jJkTAo.jpg", + "size": 108368, + "eTag": "1af324731637228cbbb0b2e8c07d4e50", + "contentType": "image/jpeg", + "userMetadata": {"content-type": "image/jpeg"}, + "versionId": "1", + "sequencer": "159AD8E6F76DD9C4", + }, + }, + "source": { + "host": "", + "port": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/12.0.3 Safari/605.1.15", + }, + } + ] +} diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py new file mode 100644 index 00000000000000..836b456dc9b5c6 --- /dev/null +++ b/tests/components/minio/test_minio.py @@ -0,0 +1,190 @@ +"""Tests for Minio Hass related code.""" +import asyncio +import json +from unittest.mock import MagicMock + +import pytest +from asynctest import patch, call + +from homeassistant.components.minio import ( + QueueListener, + DOMAIN, + CONF_HOST, + CONF_PORT, + CONF_ACCESS_KEY, + CONF_SECRET_KEY, + CONF_SECURE, + CONF_LISTEN, + CONF_LISTEN_BUCKET, +) +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +from tests.components.minio.common import TEST_EVENT + + +@pytest.fixture(name="minio_client") +def minio_client_fixture(): + """Patch Minio client.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + yield minio_client_mock + + +@pytest.fixture(name="minio_client_event") +def minio_client_event_fixture(): + """Patch helper function for minio notification stream.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + response_mock = MagicMock() + stream_mock = MagicMock() + + stream_mock.__next__.side_effect = [ + "", + "", + bytearray(json.dumps(TEST_EVENT), "utf-8"), + ] + + response_mock.stream.return_value = stream_mock + minio_client_mock._url_open.return_value = response_mock + + yield minio_client_mock + + +async def test_minio_services(hass, caplog, minio_client): + """Test Minio services.""" + hass.config.whitelist_external_dirs = set("/tmp") + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + # Call services + await hass.services.async_call( + DOMAIN, + "put", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fput_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, + "get", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fget_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, "remove", {"key": "some_key", "bucket": "some_bucket"}, blocking=True + ) + assert minio_client.remove_object.call_args == call("some_bucket", "some_key") + minio_client.reset_mock() + + +async def test_minio_listen(hass, caplog, minio_client_event): + """Test minio listen on notifications.""" + minio_client_event.presigned_get_object.return_value = "http://url" + + events = [] + + @callback + def event_callback(event): + """Handle event callbback.""" + events.append(event) + + hass.bus.async_listen("minio", event_callback) + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + CONF_LISTEN: [{CONF_LISTEN_BUCKET: "test"}], + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + while not events: + await asyncio.sleep(0) + + assert 1 == len(events) + event = events[0] + + assert DOMAIN == event.event_type + assert "s3:ObjectCreated:Put" == event.data["event_name"] + assert "5jJkTAo.jpg" == event.data["file_name"] + assert "test" == event.data["bucket"] + assert "5jJkTAo.jpg" == event.data["key"] + assert "http://url" == event.data["presigned_url"] + assert 0 == len(event.data["metadata"]) + + +async def test_queue_listener(): + """Tests QueueListener firing events on Hass event bus.""" + hass = MagicMock() + + queue_listener = QueueListener(hass) + queue_listener.start() + + queue_entry = { + "event_name": "s3:ObjectCreated:Put", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + queue_listener.queue.put(queue_entry) + + queue_listener.stop() + + call_domain, call_event = hass.bus.fire.call_args[0] + + expected_event = { + "event_name": "s3:ObjectCreated:Put", + "file_name": "some_file.jpg", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + assert DOMAIN == call_domain + assert json.dumps(expected_event, sort_keys=True) == json.dumps( + call_event, sort_keys=True + ) From c5ca43189402adb78709631f1dd37e48b020edd1 Mon Sep 17 00:00:00 2001 From: Chris Thornton <54046872+cj-thornton@users.noreply.github.com> Date: Tue, 20 Aug 2019 10:01:19 +0000 Subject: [PATCH 083/213] Add path option to SABnzbd component (#25908) * Add path option to SABnzbd component Adds an optional `path` setting to the SABnzbd component. This allows support for SABnzbd installs that use a different `url_base` (typically a reverse proxied configuration; see https://sabnzbd.org/wiki/configuration/2.3/special). This change passes the `path` along to pysabnzbd as its `web_root`, which in turn uses the path to build up it's api URLs. * Use dict.get for Sabnzbd web_root path config --- homeassistant/components/sabnzbd/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f5870e42490174..bf5e90e21f1445 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -10,6 +10,7 @@ CONF_HOST, CONF_API_KEY, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SENSORS, CONF_SSL, @@ -69,6 +70,7 @@ { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( @@ -102,17 +104,20 @@ async def async_configure_sabnzbd( host = config[CONF_HOST] port = config[CONF_PORT] + web_root = config.get(CONF_PATH) uri_scheme = "https" if use_ssl else "http" base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) if api_key is None: conf = await hass.async_add_job(load_json, hass.config.path(CONFIG_FILE)) api_key = conf.get(base_url, {}).get(CONF_API_KEY, "") - sab_api = SabnzbdApi(base_url, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + base_url, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if await async_check_sabnzbd(sab_api): async_setup_sabnzbd(hass, sab_api, config, name) else: - async_request_configuration(hass, config, base_url) + async_request_configuration(hass, config, base_url, web_root) async def async_setup(hass, config): @@ -181,7 +186,7 @@ async def async_update_sabnzbd(now): @callback -def async_request_configuration(hass, config, host): +def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" from pysabnzbd import SabnzbdApi @@ -197,7 +202,9 @@ def async_request_configuration(hass, config, host): async def async_configuration_callback(data): """Handle configuration changes.""" api_key = data.get(CONF_API_KEY) - sab_api = SabnzbdApi(host, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + host, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if not await async_check_sabnzbd(sab_api): return From 178d0d2099d1683b95dd8c3d12c86e3bc2a56160 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 20 Aug 2019 17:49:21 +0200 Subject: [PATCH 084/213] Update devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 595506c8ccc71f..ff2d586fc5a309 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDITOR='code --wait'" + "-e", "GIT_EDITOR=\"code --wait\"" ], "extensions": [ "ms-python.python", From a347a41d3c4fb54011778e214145288cc8e2ba17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 09:19:00 -0700 Subject: [PATCH 085/213] Add strings for traccar config flow (#26089) * Add strings for traccar config flow * Update strings.json --- homeassistant/components/traccar/strings.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 homeassistant/components/traccar/strings.json diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json new file mode 100644 index 00000000000000..19f4eb0da22850 --- /dev/null +++ b/homeassistant/components/traccar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Traccar", + "step": { + "user": { + "title": "Set up Traccar", + "description": "Are you sure you want to set up Traccar?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + } + } +} From 0e4504296e8aa811bf9a2ea4002cb1478d17c977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Vran=C3=ADk?= Date: Tue, 20 Aug 2019 18:26:15 +0200 Subject: [PATCH 086/213] Update pyrainbird to version 0.2.1 to fix zone number (#26064) * Update pyrainbird to version 0.2.0 to fix zone number issue: - home-assistant/home-assistant/issues/24519 - jbarrancos/pyrainbird/issues/5 - https://community.home-assistant.io/t/rainbird-zone-switches-5-8-dont-correspond/104705 * requirements_all.txt regenerated * code formatting * code formatting * response checking * fixed switch state * pyrainbird version bump * formatting * version bump * if instead elif --- homeassistant/components/rainbird/__init__.py | 5 ++--- homeassistant/components/rainbird/manifest.json | 2 +- homeassistant/components/rainbird/sensor.py | 6 +++++- homeassistant/components/rainbird/switch.py | 14 +++++++++++--- requirements_all.txt | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 3ae1c8bf585146..1d8ed8e37b169c 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -29,13 +29,12 @@ def setup(hass, config): from pyrainbird import RainbirdController - controller = RainbirdController() - controller.setConfig(server, password) + controller = RainbirdController(server, password) _LOGGER.debug("Rain Bird Controller set to: %s", server) initial_status = controller.currentIrrigation() - if initial_status == -1: + if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": _LOGGER.error("Error getting state. Possible configuration issues") return False diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 24113d6253427f..584ea22afe23ef 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,7 +3,7 @@ "name": "Rainbird", "documentation": "https://www.home-assistant.io/components/rainbird", "requirements": [ - "pyrainbird==0.1.6" + "pyrainbird==0.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index d59ea3b0fecce1..2d4549a21d5dba 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -56,7 +56,11 @@ def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) if self._sensor_type == "rainsensor": - self._state = self._controller.currentRainSensorState() + result = self._controller.currentRainSensorState() + if result and result["type"] == "CurrentRainSensorStateResponse": + self._state = result["sensorState"] + else: + self._state = None @property def name(self): diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 94b37c52fb7acb..a1b82bc1af7453 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -70,15 +70,23 @@ def name(self): def turn_on(self, **kwargs): """Turn the switch on.""" - self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + if response and response["type"] == "AcknowledgeResponse": + self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - self._rainbird.stopIrrigation() + response = self._rainbird.stopIrrigation() + if response and response["type"] == "AcknowledgeResponse": + self._state = False def get_device_status(self): """Get the status of the switch from Rain Bird Controller.""" - return self._rainbird.currentIrrigation() == self._zone + response = self._rainbird.currentIrrigation() + if response is None: + return None + if isinstance(response, dict) and "sprinklers" in response: + return response["sprinklers"][self._zone] def update(self): """Update switch status.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6cd7f5d28224e1..0b7346031d4c98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.1.6 +pyrainbird==0.2.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 From daa0330da4b4864df8c845bb160a6ad1bcad1936 Mon Sep 17 00:00:00 2001 From: legacycode Date: Tue, 20 Aug 2019 19:35:39 +0200 Subject: [PATCH 087/213] Update pylacrosse library to version 0.4.0 (#26088) * Updated pylacrosse library to version 0.4.0. Adding support for remote serial port. * Generated new requirements_all.txt file. --- homeassistant/components/lacrosse/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 4716b3cb548e67..99dd4889213361 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -3,7 +3,7 @@ "name": "Lacrosse", "documentation": "https://www.home-assistant.io/components/lacrosse", "requirements": [ - "pylacrosse==0.3.1" + "pylacrosse==0.4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0b7346031d4c98..746c23bc28ae13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1234,7 +1234,7 @@ pykira==0.1.1 pykwb==0.0.8 # homeassistant.components.lacrosse -pylacrosse==0.3.1 +pylacrosse==0.4.0 # homeassistant.components.lastfm pylast==3.1.0 From 97d3f49bb83f385db78c9e46f23cd32469c3a303 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 20 Aug 2019 19:37:55 +0200 Subject: [PATCH 088/213] Docker ADR (#26085) * Migrate Docker image to Hass.io / Multiarch * Fix sudo * Update CODEOWNERS * Fix manifest * Add more logic * fix handling * Move dockerfile * Modify options --- .devcontainer/devcontainer.json | 7 +- CODEOWNERS | 4 - Dockerfile | 38 ------ .devcontainer/Dockerfile => Dockerfile.dev | 1 + azure-pipelines-release.yml | 58 ++++++++- script/hassfest/codeowners.py | 4 - virtualization/Docker/Dockerfile.dev | 64 --------- virtualization/Docker/scripts/libcec | 47 ------- virtualization/Docker/scripts/locales | 12 -- virtualization/Docker/scripts/openalpr | 32 ----- virtualization/Docker/scripts/ssocr | 24 ---- virtualization/Docker/scripts/tellstick | 17 --- virtualization/Docker/setup_docker_prereqs | 84 ------------ virtualization/vagrant/Vagrantfile | 24 ---- virtualization/vagrant/config/.placeholder | 0 .../vagrant/home-assistant@.service | 23 ---- virtualization/vagrant/provision.bat | 50 -------- virtualization/vagrant/provision.sh | 121 ------------------ 18 files changed, 62 insertions(+), 548 deletions(-) delete mode 100644 Dockerfile rename .devcontainer/Dockerfile => Dockerfile.dev (97%) delete mode 100644 virtualization/Docker/Dockerfile.dev delete mode 100755 virtualization/Docker/scripts/libcec delete mode 100755 virtualization/Docker/scripts/locales delete mode 100755 virtualization/Docker/scripts/openalpr delete mode 100755 virtualization/Docker/scripts/ssocr delete mode 100755 virtualization/Docker/scripts/tellstick delete mode 100755 virtualization/Docker/setup_docker_prereqs delete mode 100644 virtualization/vagrant/Vagrantfile delete mode 100644 virtualization/vagrant/config/.placeholder delete mode 100644 virtualization/vagrant/home-assistant@.service delete mode 100644 virtualization/vagrant/provision.bat delete mode 100755 virtualization/vagrant/provision.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff2d586fc5a309..22bd4384b23e5e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,11 +2,12 @@ { "name": "Home Assistant Dev", "context": "..", - "dockerFile": "Dockerfile", + "dockerFile": "../Dockerfile.dev", "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDITOR=\"code --wait\"" + "-e", + "GIT_EDITOR=\"code --wait\"" ], "extensions": [ "ms-python.python", @@ -31,4 +32,4 @@ "!include_dir_merge_named scalar" ] } -} +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 1425c4764787e2..3d17b4f91362c0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,10 +9,6 @@ homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ebd802374ebda8..00000000000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Notice: -# When updating this file, please also update virtualization/Docker/Dockerfile.dev -# This way, the development image and the production image are kept in sync. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython tensorflow - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/.devcontainer/Dockerfile b/Dockerfile.dev similarity index 97% rename from .devcontainer/Dockerfile rename to Dockerfile.dev index 3bfc7e94148dcd..00f5576bdbb0fc 100644 --- a/.devcontainer/Dockerfile +++ b/Dockerfile.dev @@ -16,6 +16,7 @@ RUN apt-get update \ WORKDIR /usr/src +# Setup hass-release RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ && cd hass-release \ && pip3 install -e . diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 13a031fda15d77..1b547d5c609602 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -60,7 +60,7 @@ stages: - script: | export TWINE_USERNAME="$(twineUser)" export TWINE_PASSWORD="$(twinePassword)" - + twine upload dist/* --skip-existing displayName: 'Upload pypi' - job: 'ReleaseDocker' @@ -150,3 +150,59 @@ stages: git commit -am "Bump Home Assistant $version" git push displayName: 'Update version files' + - job: 'ReleaseDocker' + pool: + vmImage: 'ubuntu-latest' + steps: + - script: | + set -e + export DOCKER_CLI_EXPERIMENTAL=enabled + + function create_manifest() { + local tag_l=$1 + local tag_r=$2 + + sudo docker manifest create homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + homeassistant/i386-homeassistant:${tag_r} \ + homeassistant/armhf-homeassistant:${tag_r} \ + homeassistant/armv7-homeassistant:${tag_r} \ + homeassistant/aarch64-homeassistant:${tag_r} + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + --os linux --arch amd64 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/i386-homeassistant:${tag_r} \ + --os linux --arch i386 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armhf-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v6 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armv7-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v7 + + sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/aarch64-homeassistant:${tag_r} \ + --os linux --arch arm64 --variant=v8 + + sudo docker manifest push --purge homeassistant/home-assistant:${tag_l} + } + + # Create version tag + create_manifest "$(Build.SourceBranchName)" "$(Build.SourceBranchName)" + + # Create general tags + if [[ "$version" =~ d ]]; then + create_manifest "dev" "$(Build.SourceBranchName)" + elif [[ "$version" =~ b ]]; then + create_manifest "beta" "$(Build.SourceBranchName)" + else + create_manifest "stable" "$(Build.SourceBranchName)" + create_manifest "latest" "$(Build.SourceBranchName)" + fi + + displayName: 'Create Meta-Image' diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 07a1d827b33b7b..1341bd75d1b0cf 100755 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -15,10 +15,6 @@ homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev deleted file mode 100644 index 2e98c53cdf1b43..00000000000000 --- a/virtualization/Docker/Dockerfile.dev +++ /dev/null @@ -1,64 +0,0 @@ -# Dockerfile for development -# Based on the production Dockerfile, but with development additions. -# Keep this file as close as possible to the production Dockerfile, so the environments match. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_COAP no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt - -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 cchardet cython - -# BEGIN: Development additions - -# Install git -RUN apt-get update \ - && apt-get install -y --no-install-recommends git \ - && rm -rf /var/lib/apt/lists/* - -# Install nodejs -RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \ - apt-get install -y nodejs - -# Install tox -RUN pip3 install --no-cache-dir tox - -# Copy over everything required to run tox -COPY requirements_test_all.txt setup.cfg setup.py tox.ini ./ -COPY homeassistant/const.py homeassistant/const.py - -# Prefetch dependencies for tox -COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN tox -e py37 --notest - -# END: Development additions - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/virtualization/Docker/scripts/libcec b/virtualization/Docker/scripts/libcec deleted file mode 100755 index 481b3e700accb7..00000000000000 --- a/virtualization/Docker/scripts/libcec +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Sets up libcec. -# Dependencies that need to be installed: -# apt-get install cmake libudev-dev libxrandr-dev swig - -# Stop on errors -set -e - -# Load required information about the current python environment -PYTHON_LIBDIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LIBDIR"))') -PYTHON_LDLIBRARY=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LDLIBRARY"))') -PYTHON_LIBRARY="${PYTHON_LIBDIR}/${PYTHON_LDLIBRARY}" -PYTHON_INCLUDE_DIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_python_inc())') - -cd /usr/src/app/ -mkdir -p build && cd build - -if [ ! -d libcec ]; then - git clone --branch release --depth 1 https://github.com/Pulse-Eight/libcec.git -fi - -cd libcec -git checkout release -git pull -git submodule update --init src/platform - -# Build libcec platform libs -( - mkdir -p src/platform/build - cd src/platform/build - cmake .. - make - make install -) - -# Build libcec -( - mkdir -p build && cd build - - cmake \ - -DPYTHON_LIBRARY="${PYTHON_LIBRARY}" \ - -DPYTHON_INCLUDE_DIR="${PYTHON_INCLUDE_DIR}" \ - .. - make -j$(nproc) - make install - ldconfig -) diff --git a/virtualization/Docker/scripts/locales b/virtualization/Docker/scripts/locales deleted file mode 100755 index cbbe03415751c4..00000000000000 --- a/virtualization/Docker/scripts/locales +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Sets up locales. - -# Stop on errors -set -e - -apt-get update -apt-get install -y --no-install-recommends locales - -# Set the locale -sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen -locale-gen diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr deleted file mode 100755 index 38669f8175baaf..00000000000000 --- a/virtualization/Docker/scripts/openalpr +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Sets up openalpr. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.image_processing.openalpr_local - libopencv-dev libtesseract-dev libleptonica-dev liblog4cplus-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/openalpr/openalpr.git openalpr - -# Setup the build directory -cd openalpr/src/ -mkdir -p build -cd build - -# Setup the compile environment -cmake -DWITH_TESTS=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. - -# compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/ssocr b/virtualization/Docker/scripts/ssocr deleted file mode 100755 index 6778bcab90d010..00000000000000 --- a/virtualization/Docker/scripts/ssocr +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Sets up ssocr to support Seven Segments Display. - -# Stop on errors -set -e - -PACKAGES=( - libimlib2 libimlib2-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/auerswal/ssocr.git ssocr -cd ssocr/ - -# Compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/tellstick b/virtualization/Docker/scripts/tellstick deleted file mode 100755 index d35e1cac2dbd92..00000000000000 --- a/virtualization/Docker/scripts/tellstick +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Sets up tellstick. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.tellstick - libtelldus-core2 socat -) - -# Add Tellstick repository -echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list -wget -qO - http://download.telldus.com/debian/telldus-public.key | apt-key add - - -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs deleted file mode 100755 index 62ac73d366ec90..00000000000000 --- a/virtualization/Docker/setup_docker_prereqs +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# Install requirements and build dependencies for Home Assistant in Docker. - -# Stop on errors -set -e - -INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" -INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" -INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" -INSTALL_DLIB="${INSTALL_DLIB:-yes}" -INSTALL_LOCALES="${INSTALL_LOCALES:-yes}" - -# Required debian packages for running hass or components -PACKAGES=( - # build-essential is required for python pillow module on non-x86_64 arch - build-essential - # homeassistant.components.image_processing.openalpr_local - libxrandr-dev - # homeassistant.components.device_tracker.nmap_tracker - nmap net-tools libcurl3-dev - # homeassistant.components.device_tracker.bluetooth_tracker - bluetooth libglib2.0-dev libbluetooth-dev - # homeassistant.components.device_tracker.owntracks - libsodium23 - # homeassistant.components.zwave - libudev-dev - # homeassistant.components.homekit_controller - libmpc-dev libmpfr-dev libgmp-dev - # homeassistant.components.ffmpeg - ffmpeg - # homeassistant.components.stream - libavformat-dev libavcodec-dev libavdevice-dev - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - # homeassistant.components.sensor.iperf3 - iperf3 -) - -# Required debian packages for building dependencies -PACKAGES_DEV=( - cmake - git - swig -) - -# Install packages -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} - -# This is a list of scripts that install additional dependencies. If you only -# need to install a package from the official debian repository, just add it -# to the list above. Only create a script if you need compiling, manually -# downloading or a 3rd party repository. -if [ "$INSTALL_TELLSTICK" == "yes" ]; then - virtualization/Docker/scripts/tellstick -fi - -if [ "$INSTALL_OPENALPR" == "yes" ]; then - virtualization/Docker/scripts/openalpr -fi - -if [ "$INSTALL_LIBCEC" == "yes" ]; then - virtualization/Docker/scripts/libcec -fi - -if [ "$INSTALL_SSOCR" == "yes" ]; then - virtualization/Docker/scripts/ssocr -fi - -if [ "$INSTALL_DLIB" == "yes" ]; then - pip3 install --no-cache-dir "dlib>=19.5" -fi - -if [ "$INSTALL_LOCALES" == "yes" ]; then - virtualization/Docker/scripts/locales -fi - -# Remove packages -apt-get remove -y --purge ${PACKAGES_DEV[@]} -apt-get -y --purge autoremove - -# Cleanup -apt-get clean -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/src/app/build/ diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile deleted file mode 100644 index d3974d51a7aa31..00000000000000 --- a/virtualization/vagrant/Vagrantfile +++ /dev/null @@ -1,24 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure(2) do |config| - config.vm.box = "debian/contrib-stretch64" - config.vm.synced_folder "../../", "/home-assistant" - config.vm.synced_folder "./config", "/root/.homeassistant" - config.vm.network "forwarded_port", guest: 8123, host: 8123 - config.vm.provision "fix-no-tty", type: "shell" do |shell| - shell.path = "provision.sh" - end - config.vm.provider :virtualbox do |vb| - vb.cpus = 2 - vb.customize ['modifyvm', :id, '--memory', '1024'] - end - config.vm.provider :hyperv do |h, override| - override.vm.box = "generic/debian9" - override.vm.hostname = "contrib-stretch" - h.vmname = "home-assistant" - h.cpus = 2 - h.memory = 1024 - h.maxmemory = 1024 - end -end diff --git a/virtualization/vagrant/config/.placeholder b/virtualization/vagrant/config/.placeholder deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/virtualization/vagrant/home-assistant@.service b/virtualization/vagrant/home-assistant@.service deleted file mode 100644 index 91b7307f30fc9e..00000000000000 --- a/virtualization/vagrant/home-assistant@.service +++ /dev/null @@ -1,23 +0,0 @@ -# This is a simple service file for systems with systemd to tun HA as user. -# -# For details please check https://home-assistant.io/getting-started/autostart/ -# -[Unit] -Description=Home Assistant for %i -After=network.target - -[Service] -Type=simple -User=%i -# Enable the following line if you get network-related HA errors during boot -#ExecStartPre=/usr/bin/sleep 60 -# Use `whereis hass` to determine the path of hass -ExecStart=/usr/bin/hass --runner -SendSIGKILL=no -RestartForceExitStatus=100 - -# on vagrant (vboxfs), disable sendfile https://www.virtualbox.org/ticket/9069 -Environment=AIOHTTP_NOSENDFILE=1 - -[Install] -WantedBy=multi-user.target diff --git a/virtualization/vagrant/provision.bat b/virtualization/vagrant/provision.bat deleted file mode 100644 index c8174e939a1987..00000000000000 --- a/virtualization/vagrant/provision.bat +++ /dev/null @@ -1,50 +0,0 @@ -@echo off -call:main %* -goto:eof - -:usage -echo.############################################################ -echo. -echo.Use `./provision.bat` to interact with HASS. E.g: -echo. -echo.- setup the environment: `./provision.bat start` -echo.- restart HASS process: `./provision.bat restart` -echo.- run test suit: `./provision.bat tests` -echo.- destroy the host and start anew: `./provision.bat recreate` -echo. -echo.Official documentation at https://home-assistant.io/docs/installation/vagrant/ -echo. -echo.############################################################' -goto:eof - -:main -if "%*"=="setup" ( - if exist setup_done del setup_done - vagrant up --provision - copy /y nul setup_done -) else ( -if "%*"=="tests" ( - copy /y nul run_tests - vagrant provision -) else ( -if "%*"=="restart" ( - copy /y nul restart - vagrant provision -) else ( -if "%*"=="start" ( - vagrant up --provision -) else ( -if "%*"=="stop" ( - vagrant halt -) else ( -if "%*"=="destroy" ( - vagrant destroy -f -) else ( -if "%*"=="recreate" ( - if exist setup_done del setup_done - if exist restart del restart - vagrant destroy -f - vagrant up --provision -) else ( - call:usage -))))))) diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh deleted file mode 100755 index 1d2eecddc73f90..00000000000000 --- a/virtualization/vagrant/provision.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -set -e - -readonly SETUP_DONE='/home-assistant/virtualization/vagrant/setup_done' -readonly RUN_TESTS='/home-assistant/virtualization/vagrant/run_tests' -readonly RESTART='/home-assistant/virtualization/vagrant/restart' - -usage() { - echo '############################################################ - -Use `./provision.sh` to interact with HASS. E.g: - -- setup the environment: `./provision.sh start` -- restart HASS process: `./provision.sh restart` -- run test suit: `./provision.sh tests` -- destroy the host and start anew: `./provision.sh recreate` - -Official documentation at https://home-assistant.io/docs/installation/vagrant/ - -############################################################' -} - -print_done() { - echo '############################################################ - - -HASS running => http://localhost:8123/ - -' -} - -setup_error() { - echo '############################################################ -Something is off... maybe setup did not complete properly? -Please ensure setup did run correctly at least once. - -To run setup again: `./provision.sh setup` - -############################################################' - exit 1 -} - -setup() { - local hass_path='/root/venv/bin/hass' - local systemd_bin_path='/usr/bin/hass' - # Setup systemd - cp /home-assistant/virtualization/vagrant/home-assistant@.service \ - /etc/systemd/system/home-assistant.service - systemctl --system daemon-reload - systemctl enable home-assistant - systemctl stop home-assistant - # Install packages - apt-get update - apt-get install -y git rsync python3-dev python3-pip libssl-dev libffi-dev - pip3 install --upgrade virtualenv - virtualenv ~/venv - source ~/venv/bin/activate - pip3 install --upgrade tox - /home-assistant/script/setup - if ! [ -f $systemd_bin_path ]; then - ln -s $hass_path $systemd_bin_path - fi - touch $SETUP_DONE - print_done - usage -} - -run_tests() { - rm -f $RUN_TESTS - echo '############################################################' - echo; echo "Running test suite, hang on..."; echo; echo - if ! systemctl stop home-assistant; then - setup_error - fi - source ~/venv/bin/activate - rsync -a --delete \ - --exclude='*.tox' \ - --exclude='*.git' \ - --exclude='.vagrant' \ - --exclude='lib64' \ - --exclude='bin/python' \ - --exclude='bin/python3' \ - /home-assistant/ /home-assistant-tests/ - cd /home-assistant-tests && tox || true - echo '############################################################' -} - -restart() { - echo "Restarting Home Assistant..." - if ! systemctl restart home-assistant; then - setup_error - else - echo "done" - fi - rm $RESTART -} - -main() { - # If a parameter is provided, we assume it's the user interacting - # with the provider script... - case $1 in - "setup") rm -f setup_done; vagrant up --provision && touch setup_done; exit ;; - "tests") touch run_tests; vagrant provision ; exit ;; - "restart") touch restart; vagrant provision ; exit ;; - "start") vagrant up --provision ; exit ;; - "stop") vagrant halt ; exit ;; - "destroy") vagrant destroy -f ; exit ;; - "recreate") rm -f setup_done restart; vagrant destroy -f; \ - vagrant up --provision; exit ;; - esac - # ...otherwise we assume it's the Vagrant provisioner - if [ $(hostname) != "contrib-jessie" ] && [ $(hostname) != "contrib-stretch" ]; then usage; exit; fi - if ! [ -f $SETUP_DONE ]; then setup; fi - if [ -f $RESTART ]; then restart; fi - if [ -f $RUN_TESTS ]; then run_tests; fi - if ! systemctl start home-assistant; then - setup_error - fi -} - -main $* From 33c35a6c3c3675a16dae509c3df821ba1b1c3930 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 20 Aug 2019 19:43:39 +0200 Subject: [PATCH 089/213] Bump geniushub client (#26084) * bump geniushub client * delint * remove unsused lint hints --- .../components/geniushub/__init__.py | 54 +++++++++--- .../components/geniushub/binary_sensor.py | 44 +++------- homeassistant/components/geniushub/climate.py | 29 ++---- .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 88 +++++-------------- .../components/geniushub/water_heater.py | 62 ++++++------- requirements_all.txt | 2 +- 7 files changed, 113 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index e4b723d595b433..45f3f91cd6d82a 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,17 +1,23 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging +from typing import Awaitable import aiohttp import voluptuous as vol -from geniushubclient import GeniusHubClient +from geniushubclient import GeniusHub from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -45,7 +51,7 @@ async def async_setup(hass, hass_config): broker = GeniusBroker(hass, args, kwargs) try: - await broker._client.hub.update() # pylint: disable=protected-access + await broker.client.update() except aiohttp.ClientResponseError as err: _LOGGER.error("Setup failed, check your configuration, %s", err) return False @@ -58,7 +64,7 @@ async def async_setup(hass, hass_config): async_load_platform(hass, platform, DOMAIN, {}, hass_config) ) - if broker._client.api_version == 3: # pylint: disable=protected-access + if broker.client.api_version == 3: # pylint: disable=no-member for platform in ["sensor", "binary_sensor"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) @@ -72,27 +78,53 @@ class GeniusBroker: def __init__(self, hass, args, kwargs): """Initialize the geniushub client.""" - self._hass = hass - self._client = hass.data[DOMAIN]["client"] = GeniusHubClient( + self.hass = hass + self.client = hass.data[DOMAIN]["client"] = GeniusHub( *args, **kwargs, session=async_get_clientsession(hass) ) async def async_update(self, now, **kwargs): """Update the geniushub client's data.""" try: - await self._client.hub.update() + await self.client.update() except aiohttp.ClientResponseError as err: _LOGGER.warning("Update failed, %s", err) return self.make_debug_log_entries() - async_dispatcher_send(self._hass, DOMAIN) + async_dispatcher_send(self.hass, DOMAIN) def make_debug_log_entries(self): """Make any useful debug log entries.""" # pylint: disable=protected-access _LOGGER.debug( - "Raw JSON: \n\nhub._raw_zones = %s \n\nhub._raw_devices = %s", - self._client.hub._raw_zones, - self._client.hub._raw_devices, + "Raw JSON: \n\nclient._zones = %s \n\nclient._devices = %s", + self.client._zones, + self.client._devices, ) + + +class GeniusEntity(Entity): + """Base for all Genius Hub endtities.""" + + def __init__(self): + """Initialize the entity.""" + self._name = None + + async def async_added_to_hass(self) -> Awaitable[None]: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self) -> str: + """Return the name of the geniushub entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return False as geniushub entities should not be polled.""" + return False diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index feb2e0da33e4d5..1cc8cd3f4063b0 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Genius Hub binary_sensor devices.""" +from typing import Any, Dict + from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utc_from_timestamp -from . import DOMAIN +from . import DOMAIN, GeniusEntity GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] @@ -14,58 +14,38 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= client = hass.data[DOMAIN]["client"] switches = [ - GeniusBinarySensor(client, d) - for d in client.hub.device_objs - if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH ] async_add_entities(switches) -class GeniusBinarySensor(BinarySensorDevice): +class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the binary sensor.""" - self._client = client - self._device = device + super().__init__() + self._device = device if device.type[:21] == "Dual Channel Receiver": self._name = "Dual Channel Receiver {}".format(device.id) else: self._name = "{} {}".format(device.type, device.id) - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - @property - def is_on(self): + def is_on(self) -> bool: """Return the status of the sensor.""" return self._device.data["state"]["outputOnOff"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] if last_comms != 0: attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index cee737c09f8081..a856e48438fcd6 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -11,10 +11,8 @@ SUPPORT_PRESET_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, GeniusEntity ATTR_DURATION = "duration" @@ -38,32 +36,24 @@ async def async_setup_platform( client = hass.data[DOMAIN]["client"] entities = [ - GeniusClimateZone(client, z) for z in client.hub.zone_objs if z.type in GH_ZONES + GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES ] async_add_entities(entities) -class GeniusClimateZone(ClimateDevice): +class GeniusClimateZone(GeniusEntity, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, client, zone): + def __init__(self, zone) -> None: """Initialize the climate device.""" - self._client = client - self._zone = zone + super().__init__() + self._zone = zone if hasattr(self._zone, "occupied"): # has a movement sensor self._preset_modes = list(HA_PRESET_TO_GH) else: self._preset_modes = [PRESET_BOOST] - async def async_added_to_hass(self) -> Awaitable[None]: - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - @property def name(self) -> str: """Return the name of the climate device.""" @@ -75,11 +65,6 @@ def device_state_attributes(self) -> Dict[str, Any]: tmp = self._zone.data.items() return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" @@ -91,7 +76,7 @@ def current_temperature(self) -> Optional[float]: return self._zone.data["temperature"] @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._zone.data["setpoint"] diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 0721c4ff3893ba..12f7c266840bf6 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.5.8" + "geniushub-client==0.6.5" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 65bfcb7fe9bed7..5e39be1620a983 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,13 +1,11 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta +from typing import Any, Awaitable, Dict from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp, utcnow -from . import DOMAIN +from . import DOMAIN, GeniusEntity GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] @@ -22,44 +20,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Genius Hub sensor entities.""" client = hass.data[DOMAIN]["client"] - sensors = [ - GeniusBattery(client, d) - for d in client.hub.device_objs - if d.type in GH_HAS_BATTERY - ] + sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(Entity): +class GeniusBattery(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the sensor.""" - self._client = client - self._device = device + super().__init__() + self._device = device self._name = "{} {}".format(device.type, device.id) - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the sensor.""" - # noqa; pylint: disable=protected-access - values = self._device._raw_data["childValues"] + + values = self._device._raw["childValues"] # pylint: disable=protected-access last_comms = utc_from_timestamp(values["lastComms"]["val"]) if "WakeUp_Interval" in values: @@ -83,78 +64,57 @@ def icon(self): return icon @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return "%" @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def state(self): + def state(self) -> str: """Return the state of the sensor.""" level = self._device.data["state"].get("batteryLevel", 255) return level if level != 255 else 0 @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_data["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} -class GeniusIssue(Entity): +class GeniusIssue(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, level): + def __init__(self, hub, level) -> None: """Initialize the sensor.""" - self._hub = client.hub + super().__init__() + + self._hub = hub self._name = GH_LEVEL_MAPPING[level] self._level = level self._issues = [] - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - @property - def state(self): + def state(self) -> str: """Return the number of issues.""" return len(self._issues) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" return {"{}_list".format(self._level): self._issues} - async def async_update(self): + async def async_update(self) -> Awaitable[None]: """Process the sensor's state data.""" self._issues = [ i["description"] for i in self._hub.issues if i["level"] == self._level diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index feb4235d4dd264..1086160e77c864 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,14 +1,14 @@ """Support for Genius Hub water_heater devices.""" +from typing import Any, Awaitable, Dict, Optional, List + from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, GeniusEntity STATE_AUTO = "auto" STATE_MANUAL = "manual" @@ -44,93 +44,81 @@ async def async_setup_platform( client = hass.data[DOMAIN]["client"] entities = [ - GeniusWaterHeater(client, z) - for z in client.hub.zone_objs - if z.type in GH_HEATERS + GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS ] async_add_entities(entities) -class GeniusWaterHeater(WaterHeaterDevice): +class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, client, boiler): + def __init__(self, boiler) -> None: """Initialize the water_heater device.""" - self._client = client - self._boiler = boiler + super().__init__() + self._boiler = boiler self._operation_list = list(HA_OPMODE_TO_GH) - async def async_added_to_hass(self): - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): + def name(self) -> str: """Return the name of the water_heater device.""" return self._boiler.name @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - tmp = self._boiler.data.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False + return { + "status": { + k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS + } + } @property - def current_temperature(self): + def current_temperature(self) -> Optional[float]: """Return the current temperature.""" return self._boiler.data.get("temperature") @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._boiler.data["setpoint"] @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MIN_TEMP @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MAX_TEMP @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return GH_SUPPORT_FLAGS @property - def operation_list(self): + def operation_list(self) -> List[str]: """Return the list of available operation modes.""" return self._operation_list @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" return GH_STATE_TO_HA[self._boiler.data["mode"]] - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: """Set a new operation mode for this boiler.""" await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> Awaitable[None]: """Set a new target temperature for this boiler.""" temperature = kwargs[ATTR_TEMPERATURE] await self._boiler.set_override(temperature, 3600) # 1 hour diff --git a/requirements_all.txt b/requirements_all.txt index 746c23bc28ae13..657a79121598f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.5.8 +geniushub-client==0.6.5 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From cf2d927f141e195b1c3ca15ca6c5e625994e208f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 10:46:51 -0700 Subject: [PATCH 090/213] Use init_subclass for Config Entries (#26059) * Use init_subclass for Config Entries * Ignore type --- homeassistant/components/hue/config_flow.py | 3 +- homeassistant/components/met/config_flow.py | 3 +- homeassistant/config_entries.py | 6 ++++ tests/test_config_entries.py | 35 +++++++++++---------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 1d058d84b61060..0b0e3723b138a1 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -44,8 +44,7 @@ def _find_username_from_config(hass, filename): return None -@config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlow): +class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" VERSION = 1 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index e903c717e64f46..795ba57d9887b2 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -17,8 +17,7 @@ def configured_instances(hass): ) -@config_entries.HANDLERS.register(DOMAIN) -class MetFlowHandler(config_entries.ConfigFlow): +class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Met component.""" VERSION = 1 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 87bce1a870c249..2e1fbea14d1392 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -670,6 +670,12 @@ async def _old_conf_migrator(old_config): class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" + def __init_subclass__(cls, domain=None, **kwargs): + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) # type: ignore + if domain is not None: + HANDLERS.register(domain)(cls) + CONNECTION_CLASS = CONN_CLASS_UNKNOWN @staticmethod diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6c1b00693dd75e..ca6872a7a2cc1e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -521,31 +521,32 @@ async def test_discovery_notification(hass): mock_entity_platform(hass, "config_flow.test", None) await async_setup_component(hass, "persistent_notification", {}) - class TestFlow(config_entries.ConfigFlow): - VERSION = 5 + with patch.dict(config_entries.HANDLERS): - async def async_step_discovery(self, user_input=None): - if user_input is not None: - return self.async_create_entry( - title="Test Title", data={"token": "abcd"} - ) - return self.async_show_form(step_id="discovery") + class TestFlow(config_entries.ConfigFlow, domain="test"): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title="Test Title", data={"token": "abcd"} + ) + return self.async_show_form(step_id="discovery") - with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is not None - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is None async def test_discovery_notification_not_created(hass): From 7fd53ac912484f5da13c256c84206187a4517805 Mon Sep 17 00:00:00 2001 From: ahertz Date: Tue, 20 Aug 2019 13:53:45 -0400 Subject: [PATCH 091/213] Sonos playlists play media (#26054) * Add handling for Sonos playlists in media_player.play_media * Avoid breaking change by falling back to previous behavior * Use the proper MEDIA_TYPE_PLAYLIST constant. * Addressed comments, restricting media_type to music or playlist --- .../components/sonos/media_player.py | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a4d461f289f243..86e30621334579 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -938,20 +939,35 @@ def play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. + If media_type is "playlist", media_id should be a Sonos + Playlist name. Otherwise, media_id should be a URI. + If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if media_type == MEDIA_TYPE_MUSIC: + if kwargs.get(ATTR_MEDIA_ENQUEUE): + try: + self.soco.add_uri_to_queue(media_id) + except SoCoUPnPException: + _LOGGER.error( + 'Error parsing media uri "%s", ' + "please check it's a valid media resource " + "supported by Sonos", + media_id, + ) + else: + self.soco.play_uri(media_id) + elif media_type == MEDIA_TYPE_PLAYLIST: try: - self.soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error( - 'Error parsing media uri "%s", ' - "please check it's a valid media resource " - "supported by Sonos", - media_id, - ) + playlists = self.soco.get_sonos_playlists() + playlist = next(p for p in playlists if p.title == media_id) + self.soco.clear_queue() + self.soco.add_to_queue(playlist) + self.soco.play_from_queue(0) + except StopIteration: + _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: - self.soco.play_uri(media_id) + _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() def join(self, slaves): From d96edea6e297f6789b70bb1677ec3360627320e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 20 Aug 2019 19:54:54 +0200 Subject: [PATCH 092/213] Remove the googlehome integration (#26035) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/googlehome/__init__.py | 118 ------------------ .../components/googlehome/device_tracker.py | 80 ------------ .../components/googlehome/manifest.json | 12 -- homeassistant/components/googlehome/sensor.py | 91 -------------- requirements_all.txt | 3 - 7 files changed, 306 deletions(-) delete mode 100644 homeassistant/components/googlehome/__init__.py delete mode 100644 homeassistant/components/googlehome/device_tracker.py delete mode 100644 homeassistant/components/googlehome/manifest.json delete mode 100644 homeassistant/components/googlehome/sensor.py diff --git a/.coveragerc b/.coveragerc index e0bbbd66d7cf9b..1d861d69c1dfe1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -241,7 +241,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_travel_time/sensor.py - homeassistant/components/googlehome/* homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greeneye_monitor/* diff --git a/CODEOWNERS b/CODEOWNERS index 3d17b4f91362c0..d08ac85941cf94 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,7 +104,6 @@ homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 -homeassistant/components/googlehome/* @ludeeus homeassistant/components/gpsd/* @fabaff homeassistant/components/group/* @home-assistant/core homeassistant/components/gtfs/* @robbiet480 diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py deleted file mode 100644 index 01e17708fb35c6..00000000000000 --- a/homeassistant/components/googlehome/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Support Google Home units.""" -import logging - -import asyncio -import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "googlehome" -CLIENT = "googlehome_client" - -NAME = "GoogleHome" - -CONF_DEVICE_TYPES = "device_types" -CONF_RSSI_THRESHOLD = "rssi_threshold" -CONF_TRACK_ALARMS = "track_alarms" -CONF_TRACK_DEVICES = "track_devices" - -DEVICE_TYPES = [1, 2, 3] -DEFAULT_RSSI_THRESHOLD = -70 - -DEVICE_CONFIG = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): vol.All( - cv.ensure_list, [vol.In(DEVICE_TYPES)] - ), - vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): vol.Coerce( - int - ), - vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, - } -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG])} - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the Google Home component.""" - hass.data[DOMAIN] = {} - hass.data[CLIENT] = GoogleHomeClient(hass) - - for device in config[DOMAIN][CONF_DEVICES]: - hass.data[DOMAIN][device["host"]] = {} - if device[CONF_TRACK_DEVICES]: - hass.async_create_task( - discovery.async_load_platform( - hass, "device_tracker", DOMAIN, device, config - ) - ) - - if device[CONF_TRACK_ALARMS]: - hass.async_create_task( - discovery.async_load_platform(hass, "sensor", DOMAIN, device, config) - ) - - return True - - -class GoogleHomeClient: - """Handle all communication with the Google Home unit.""" - - def __init__(self, hass): - """Initialize the Google Home Client.""" - self.hass = hass - self._connected = None - - async def update_info(self, host): - """Update data from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home info for %s", host) - session = async_get_clientsession(self.hass) - - device_info = await Cast(host, self.hass.loop, session).info() - device_info_data = await device_info.get_device_info() - self._connected = bool(device_info_data) - - self.hass.data[DOMAIN][host]["info"] = device_info_data - - async def update_bluetooth(self, host): - """Update bluetooth from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - bluetooth = await Cast(host, self.hass.loop, session).bluetooth() - await bluetooth.scan_for_devices() - await asyncio.sleep(5) - bluetooth_data = await bluetooth.get_scan_result() - - self.hass.data[DOMAIN][host]["bluetooth"] = bluetooth_data - - async def update_alarms(self, host): - """Update alarms from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - assistant = await Cast(host, self.hass.loop, session).assistant() - alarms_data = await assistant.get_alarms() - - self.hass.data[DOMAIN][host]["alarms"] = alarms_data diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py deleted file mode 100644 index 58350afa430904..00000000000000 --- a/homeassistant/components/googlehome/device_tracker.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Google Home Bluetooth tacker.""" -from datetime import timedelta -import logging - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Validate the configuration and return a Google Home scanner.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return False - scanner = GoogleHomeDeviceScanner( - hass, hass.data[CLIENT], discovery_info, async_see - ) - return await scanner.async_init() - - -class GoogleHomeDeviceScanner(DeviceScanner): - """This class queries a Google Home unit.""" - - def __init__(self, hass, client, config, async_see): - """Initialize the scanner.""" - self.async_see = async_see - self.hass = hass - self.rssi = config["rssi_threshold"] - self.device_types = config["device_types"] - self.host = config["host"] - self.client = client - - async def async_init(self): - """Further initialize connection to Google Home.""" - await self.client.update_info(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info", {}) - connected = bool(info) - if connected: - await self.async_update() - async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return connected - - async def async_update(self, now=None): - """Ensure the information from Google Home is up to date.""" - _LOGGER.debug("Checking Devices on %s", self.host) - await self.client.update_bluetooth(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info") - bluetooth = data.get("bluetooth") - if info is None or bluetooth is None: - return - google_home_name = info.get("name", NAME) - - for device in bluetooth: - if ( - device["device_type"] not in self.device_types - or device["rssi"] < self.rssi - ): - continue - - name = "{} {}".format(self.host, device["mac_address"]) - - attributes = {} - attributes["btle_mac_address"] = device["mac_address"] - attributes["ghname"] = google_home_name - attributes["rssi"] = device["rssi"] - attributes["source_type"] = "bluetooth" - if device["name"]: - attributes["name"] = device["name"] - - await self.async_see(dev_id=slugify(name), attributes=attributes) diff --git a/homeassistant/components/googlehome/manifest.json b/homeassistant/components/googlehome/manifest.json deleted file mode 100644 index 107e7d634f0f01..00000000000000 --- a/homeassistant/components/googlehome/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "googlehome", - "name": "Googlehome", - "documentation": "https://www.home-assistant.io/components/googlehome", - "requirements": [ - "googledevices==1.0.2" - ], - "dependencies": [], - "codeowners": [ - "@ludeeus" - ] -} diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py deleted file mode 100644 index 6a578e14f5ae35..00000000000000 --- a/homeassistant/components/googlehome/sensor.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support for Google Home alarm sensor.""" -from datetime import timedelta -import logging - -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity -import homeassistant.util.dt as dt_util - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -SCAN_INTERVAL = timedelta(seconds=10) - -_LOGGER = logging.getLogger(__name__) - -ICON = "mdi:alarm" - -SENSOR_TYPES = {"timer": "Timer", "alarm": "Alarm"} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the googlehome sensor platform.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return - - await hass.data[CLIENT].update_info(discovery_info["host"]) - data = hass.data[GOOGLEHOME_DOMAIN][discovery_info["host"]] - info = data.get("info", {}) - - devices = [] - for condition in SENSOR_TYPES: - device = GoogleHomeAlarm( - hass.data[CLIENT], condition, discovery_info, info.get("name", NAME) - ) - devices.append(device) - - async_add_entities(devices, True) - - -class GoogleHomeAlarm(Entity): - """Representation of a GoogleHomeAlarm.""" - - def __init__(self, client, condition, config, name): - """Initialize the GoogleHomeAlarm sensor.""" - self._host = config["host"] - self._client = client - self._condition = condition - self._name = None - self._state = None - self._available = True - self._name = "{} {}".format(name, SENSOR_TYPES[self._condition]) - - async def async_update(self): - """Update the data.""" - await self._client.update_alarms(self._host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self._host] - - alarms = data.get("alarms")[self._condition] - if not alarms: - self._available = False - return - self._available = True - time_date = dt_util.utc_from_timestamp( - min(element["fire_time"] for element in alarms) / 1000 - ) - self._state = time_date.isoformat() - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def available(self): - """Return the availability state.""" - return self._available - - @property - def icon(self): - """Return the icon.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 657a79121598f8..7b09fdddbe5aa9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,9 +560,6 @@ google-cloud-pubsub==0.39.1 # homeassistant.components.google_cloud google-cloud-texttospeech==0.4.0 -# homeassistant.components.googlehome -googledevices==1.0.2 - # homeassistant.components.google_travel_time googlemaps==2.5.1 From 4bce1efeeede25c84337e06d3ef97233e32dea72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 20 Aug 2019 20:55:40 +0300 Subject: [PATCH 093/213] Test with 3.6.0 in Travis (#26039) https://github.com/home-assistant/home-assistant/pull/26030#issuecomment-522298190 --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f54f4027de4ea1..3447571a3e8571 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,18 @@ addons: matrix: fast_finish: true include: - - python: "3.6" + - python: "3.6.0" env: TOXENV=lint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=pylint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=typing - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=py36 + dist: trusty - python: "3.7" env: TOXENV=py37 From f34b84a5cf0dead8a5057f63bf435a84640f0554 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 20 Aug 2019 19:59:01 +0200 Subject: [PATCH 094/213] add id to state attributes (#26086) --- homeassistant/components/homematicip_cloud/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 021c264f63f608..6ff39c8b3a7585 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -11,6 +11,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_ID = "id" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP @@ -93,7 +94,8 @@ def icon(self) -> Optional[str]: @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType} + attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_ID: self._device.id} + if hasattr(self._device, "sabotage") and self._device.sabotage: attr[ATTR_SABOTAGE] = self._device.sabotage if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: From 5c91a6cd48cfcb57c64333b6368e6406f8cd8d78 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 12:43:50 -0700 Subject: [PATCH 095/213] Fix open-ui cli arg (#26091) * Fix open-ui cli command * Align add_job typing with async_add_job --- homeassistant/__main__.py | 26 ++++---------------------- homeassistant/core.py | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d21bfb5a71a322..8ec2a8c2d3cb98 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -10,12 +10,7 @@ from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import from homeassistant import monkey_patch -from homeassistant.const import ( - __version__, - EVENT_HOMEASSISTANT_START, - REQUIRED_PYTHON_VER, - RESTART_EXIT_CODE, -) +from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE if TYPE_CHECKING: from homeassistant import core @@ -309,23 +304,10 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: log_no_color=args.log_no_color, ) - if args.open_ui: - # Imported here to avoid importing asyncio before monkey patch - from homeassistant.util.async_ import run_callback_threadsafe + if args.open_ui and hass.config.api is not None: + import webbrowser - def open_browser(_: Any) -> None: - """Open the web interface in a browser.""" - if hass.config.api is not None: - import webbrowser - - webbrowser.open(hass.config.api.base_url) - - run_callback_threadsafe( - hass.loop, - hass.bus.async_listen_once, - EVENT_HOMEASSISTANT_START, - open_browser, - ) + hass.add_job(webbrowser.open, hass.config.api.base_url) return await hass.async_run() diff --git a/homeassistant/core.py b/homeassistant/core.py index a205aa401a6a52..e8e33a0479eb61 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -276,7 +276,7 @@ async def async_start(self) -> None: self.state = CoreState.running _async_create_timer(self) - def add_job(self, target: Callable[..., None], *args: Any) -> None: + def add_job(self, target: Callable[..., Any], *args: Any) -> None: """Add job to the executor pool. target: target to call. From 000d3d4fdec1e1cf936e75afc43c7ab43e7d808a Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 20 Aug 2019 23:27:59 +0200 Subject: [PATCH 096/213] [bugfix] Exception if vendor's servers are currently unavailable (#26093) --- homeassistant/components/evohome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index f0e7a26e1f56b0..0530878236236a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -273,10 +273,10 @@ def update(self, *args, **kwargs) -> None: else: self.timers["statusUpdated"] = utcnow() - _LOGGER.debug("Status = %s", status) + _LOGGER.debug("Status = %s", status) - # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) + # inform the evohome devices that state data has been updated + async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) class EvoDevice(Entity): From 21a946009d68238004253ec7b8f9b0e7dac22f48 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 20 Aug 2019 19:06:38 -0400 Subject: [PATCH 097/213] Bump up zha dependencies. (#26097) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4c45b59aebb253..dcf38e63d2f3e6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,9 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.9.0", + "bellows-homeassistant==0.9.1", "zha-quirks==0.0.20", - "zigpy-deconz==0.2.1", + "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", "zigpy-zigate==0.1.0" diff --git a/requirements_all.txt b/requirements_all.txt index 7b09fdddbe5aa9..78a17c42a452bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,7 +263,7 @@ batinfo==0.4.2 beautifulsoup4==4.8.0 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -1980,7 +1980,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.2.1 +zigpy-deconz==0.2.2 # homeassistant.components.zha zigpy-homeassistant==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2829d2018c674..6e53f3badd7caf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -92,7 +92,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.caldav caldav==0.6.1 From 2fbe01fb33ca291a645a3177cbb3412aa158374f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Aug 2019 16:20:38 -0700 Subject: [PATCH 098/213] Updated frontend to 20190820.1 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f7c1a4f874f096..2337c3cb469217 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190820.0" + "home-assistant-frontend==20190820.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 74ffb83f884514..2c649b007e3ecb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 78a17c42a452bc..70378e90fdaeb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e53f3badd7caf..5380f53c979e3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.0 +home-assistant-frontend==20190820.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 38ce4039c3c6580e13d9ad006f3390e2c9affd7a Mon Sep 17 00:00:00 2001 From: Gerard Date: Wed, 21 Aug 2019 09:11:06 +0200 Subject: [PATCH 099/213] Update bimmer_connected to 0.6.0 (#26098) * Update bimmer_connected to 0.6.0 * Correct file properties --- CODEOWNERS | 1 + .../bmw_connected_drive/__init__.py | 5 +- .../bmw_connected_drive/binary_sensor.py | 65 ++++++++++--------- .../bmw_connected_drive/manifest.json | 7 +- .../components/bmw_connected_drive/sensor.py | 13 ++-- requirements_all.txt | 2 +- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d08ac85941cf94..7a153b3ff82b0d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,6 +40,7 @@ homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot +homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 9b44012e7580fd..c257470bb2d0e8 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -143,7 +143,10 @@ def update(self, *_): for listener in self._update_listeners: listener() except IOError as exception: - _LOGGER.error("Error updating the vehicle state") + _LOGGER.error( + "Could not connect to the BMW Connected Drive portal. " + "The vehicle state could not be updated." + ) _LOGGER.exception(exception) def add_update_listener(self, listener): diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d52bec330fba78..418ccbabffe17b 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,17 +9,17 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening"], - "windows": ["Windows", "opening"], - "door_lock_state": ["Door lock state", "safety"], - "lights_parking": ["Parking lights", "light"], - "condition_based_services": ["Condition based services", "problem"], - "check_control_messages": ["Control messages", "problem"], + "lids": ["Doors", "opening", "mdi:car-door"], + "windows": ["Windows", "opening", "mdi:car-door"], + "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], + "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], + "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], } SENSOR_TYPES_ELEC = { - "charging_status": ["Charging status", "power"], - "connection_status": ["Connection status", "plug"], + "charging_status": ["Charging status", "power", "mdi:ev-station"], + "connection_status": ["Connection status", "plug", "mdi:car-electric"], } SENSOR_TYPES_ELEC.update(SENSOR_TYPES) @@ -35,24 +35,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if vehicle.has_hv_battery: _LOGGER.debug("BMW with a high voltage battery") for key, value in sorted(SENSOR_TYPES_ELEC.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) elif vehicle.has_internal_combustion_engine: _LOGGER.debug("BMW with an internal combustion engine") for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) add_entities(devices, True) class BMWConnectedDriveSensor(BinarySensorDevice): """Representation of a BMW vehicle binary sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, device_class): + def __init__( + self, account, vehicle, attribute: str, sensor_name, device_class, icon + ): """Constructor.""" self._account = account self._vehicle = vehicle @@ -61,6 +65,7 @@ def __init__(self, account, vehicle, attribute: str, sensor_name, device_class): self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._device_class = device_class + self._icon = icon self._state = None @property @@ -81,6 +86,11 @@ def name(self): """Return the name of the binary sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def device_class(self): """Return the class of the binary sensor.""" @@ -112,23 +122,19 @@ def device_state_attributes(self): for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": - check_control_messages = vehicle_state.check_control_messages - if not check_control_messages: - result["check_control_messages"] = "OK" - else: + check_control_messages = vehicle_state.has_check_control_messages + if check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) result["check_control_messages"] = cbs_list + else: + result["check_control_messages"] = "OK" elif self._attribute == "charging_status": result["charging_status"] = vehicle_state.charging_status.value - # pylint: disable=protected-access - result["last_charging_end_result"] = vehicle_state._attributes[ - "lastChargingEndResult" - ] - if self._attribute == "connection_status": - # pylint: disable=protected-access - result["connection_status"] = vehicle_state._attributes["connectionStatus"] + result["last_charging_end_result"] = vehicle_state.last_charging_end_result + elif self._attribute == "connection_status": + result["connection_status"] = vehicle_state.connection_status return sorted(result.items()) @@ -166,8 +172,7 @@ def update(self): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == "connection_status": - # pylint: disable=protected-access - self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED" + self._state = vehicle_state.connection_status == "CONNECTED" def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index eec81aa65250ee..0cc875c50f9f51 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,11 +1,12 @@ { "domain": "bmw_connected_drive", - "name": "Bmw connected drive", + "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.3" + "bimmer_connected==0.6.0" ], "dependencies": [], "codeowners": [ + "@gerard33" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index bc133fa403416a..8248ded4f8bcef 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -51,14 +51,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in accounts: for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - devices.append(device) - device = BMWConnectedDriveSensor( - account, vehicle, "mileage", attribute_info - ) - devices.append(device) + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + devices.append(device) add_entities(devices, True) diff --git a/requirements_all.txt b/requirements_all.txt index 70378e90fdaeb5..bb12466cd71e6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -266,7 +266,7 @@ beautifulsoup4==4.8.0 bellows-homeassistant==0.9.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.3 +bimmer_connected==0.6.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 From f80c9c93cab41215629a408f3f33097cf3141d1e Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 21 Aug 2019 16:31:55 +0200 Subject: [PATCH 100/213] Add support for warning attributes to Homematic IP Cloud (#26103) * add supported optional features * use recommendations --- .../components/homematicip_cloud/device.py | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6ff39c8b3a7585..0fffad8e97eff8 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -18,6 +18,28 @@ ATTR_RSSI_PEER = "rssi_peer" ATTR_SABOTAGE = "sabotage" ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable" +ATTR_DEVICE_OVERHEATED = "device_overheated" +ATTR_DEVICE_OVERLOADED = "device_overloaded" +ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage" + +DEVICE_ATTRIBUTE_ICONS = { + "lowBat": "mdi:battery-outline", + "sabotage": "mdi:alert", + "deviceOverheated": "mdi:alert", + "deviceOverloaded": "mdi:alert", + "deviceUndervoltage": "mdi:alert", +} + +DEVICE_ATTRIBUTES = { + "modelType": ATTR_MODEL_TYPE, + "id": ATTR_ID, + "sabotage": ATTR_SABOTAGE, + "rssiDeviceValue": ATTR_RSSI_DEVICE, + "rssiPeerValue": ATTR_RSSI_PEER, + "deviceOverheated": ATTR_DEVICE_OVERHEATED, + "deviceOverloaded": ATTR_DEVICE_OVERLOADED, + "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, +} class HomematicipGenericDevice(Entity): @@ -85,21 +107,19 @@ def unique_id(self) -> str: @property def icon(self) -> Optional[str]: """Return the icon.""" - if hasattr(self._device, "lowBat") and self._device.lowBat: - return "mdi:battery-outline" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return "mdi:alert" + for attr, icon in DEVICE_ATTRIBUTE_ICONS.items(): + if getattr(self._device, attr, None): + return icon + return None @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_ID: self._device.id} - - if hasattr(self._device, "sabotage") and self._device.sabotage: - attr[ATTR_SABOTAGE] = self._device.sabotage - if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: - attr[ATTR_RSSI_DEVICE] = self._device.rssiDeviceValue - if hasattr(self._device, "rssiPeerValue") and self._device.rssiPeerValue: - attr[ATTR_RSSI_PEER] = self._device.rssiPeerValue - return attr + state_attr = {} + for attr, attr_key in DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr From cf505c65b44d346ca2866212959d89356a2052d5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 21 Aug 2019 17:12:31 +0200 Subject: [PATCH 101/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 1b547d5c609602..81bb1944bed2d0 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -154,6 +154,13 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - script: | + echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json + sudo service docker restart + + sleep 15 + sudo docker login -u $(dockerUser) -p $(dockerPassword) + displayName: 'Enable manifest / Docker login' - script: | set -e export DOCKER_CLI_EXPERIMENTAL=enabled From 08d797edba563efec5983a7b506229066d6a1e7a Mon Sep 17 00:00:00 2001 From: croghostrider Date: Wed, 21 Aug 2019 17:42:26 +0200 Subject: [PATCH 102/213] check if a light supports brightness (#26055) Fix black Fix black --- .../components/emulated_hue/hue_api.py | 28 +++++++++++++------ tests/components/emulated_hue/test_hue_api.py | 14 ++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 1b08b43c9af63b..fc00746fc7f493 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -562,17 +562,27 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: + return { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", + "name": config.get_entity_name(entity), + "modelid": "HASS123", + "uniqueid": entity.entity_id, + "swversion": "123", + } return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", + "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, + "type": "On/off light", "name": config.get_entity_name(entity), - "modelid": "HASS123", + "modelid": "HASS321", "uniqueid": entity.entity_id, "swversion": "123", } diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 57f29a4ef6180b..02f24f5afba701 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -128,6 +128,9 @@ def hass_hue(loop, hass): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs ) + # create a lamp without brightness support + hass.states.async_set("light.no_brightness", "on", {}) + # Ceiling Fan is explicitly excluded from being exposed ceiling_fan_entity = hass.states.get("fan.ceiling_fan") attrs = dict(ceiling_fan_entity.attributes) @@ -218,6 +221,17 @@ def test_discover_lights(hue_client): assert "climate.ecobee" not in devices +@asyncio.coroutine +def test_light_without_brightness_supported(hass_hue, hue_client): + """Test that light without brightness is supported.""" + light_without_brightness_json = yield from perform_get_light_state( + hue_client, "light.no_brightness", 200 + ) + + assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True + assert light_without_brightness_json["type"] == "On/off light" + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" From 46bfd5e9c832e260565d80bcc76f29c00e47b8b9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 21 Aug 2019 09:13:04 -0700 Subject: [PATCH 103/213] bump quirks (#26106) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index dcf38e63d2f3e6..0e00489303322c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.9.1", - "zha-quirks==0.0.20", + "zha-quirks==0.0.21", "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", diff --git a/requirements_all.txt b/requirements_all.txt index bb12466cd71e6d..4a4c77339dc101 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.20 +zha-quirks==0.0.21 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 39d62b43ac86bc69eca5ac08dee136e508913df8 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 21 Aug 2019 18:13:40 +0200 Subject: [PATCH 104/213] Upgrade ruamel_yaml to 0.15.100 (#26095) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2c649b007e3ecb..0dd9b6d7802629 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 +ruamel.yaml==0.15.100 sqlalchemy==1.3.7 voluptuous-serialize==2.2.0 voluptuous==0.11.7 diff --git a/requirements_all.txt b/requirements_all.txt index 4a4c77339dc101..17a707dedf8ad5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -15,7 +15,7 @@ python-slugify==3.0.3 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 +ruamel.yaml==0.15.100 voluptuous==0.11.7 voluptuous-serialize==2.2.0 diff --git a/setup.py b/setup.py index 01288c01bf125d..5ab8d74c64cf00 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ "pytz>=2019.02", "pyyaml==5.1.2", "requests==2.22.0", - "ruamel.yaml==0.15.99", + "ruamel.yaml==0.15.100", "voluptuous==0.11.7", "voluptuous-serialize==2.2.0", ] From 8f044cf52f491e0a1b92e7d4b6e5f5940c089e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:48:46 +0300 Subject: [PATCH 105/213] Upgrade pydocstyle to 4.0.1 (#26111) https://github.com/PyCQA/pydocstyle/blob/4.0.1/docs/release_notes.rst#401---august-14th-2019 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 77162b55d7b2b0..5dbb89ddcf4fc6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ flake8==3.7.8 mock-open==1.3.1 mypy==0.720 pre-commit==1.18.2 -pydocstyle==4.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5380f53c979e3a..5169b5bb56f663 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,7 +13,7 @@ flake8==3.7.8 mock-open==1.3.1 mypy==0.720 pre-commit==1.18.2 -pydocstyle==4.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 From 48e9e53f66fd23ce035d01849c5e0d5e098cd347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:49:25 +0300 Subject: [PATCH 106/213] Upgrade pytest to 5.1.1 (#26112) https://docs.pytest.org/en/latest/changelog.html#pytest-5-1-0-2019-08-15 https://docs.pytest.org/en/latest/changelog.html#pytest-5-1-1-2019-08-20 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5dbb89ddcf4fc6..ef18759d0e8ad5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,5 +18,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5169b5bb56f663..23262689c0cc68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 From e033e46161cbcc69cba787c58f2d2d6e74d4b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:49:46 +0300 Subject: [PATCH 107/213] Remove coveralls test dependency (#26110) Outdated and unused. --- requirements_test.txt | 1 - requirements_test_all.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index ef18759d0e8ad5..bfe459b0cfb8d3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,6 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23262689c0cc68..24f383d81cc610 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,6 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 From 9b1315d8e55f0ca906c4c8a1b2ae8c2ea511dc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 21 Aug 2019 19:50:26 +0300 Subject: [PATCH 108/213] Check and fix test suite leaving files behind (#25981) * azure: run check_dirty at end of tests * Fix ps4 media player tests to not write to files * .gitignore coverage.xml and test-results.xml --- .gitignore | 2 + azure-pipelines-ci.yml | 2 + tests/components/ps4/test_media_player.py | 47 ++++++++++++++++------- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 65b325a0a4bdfc..85e66ce829c5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -59,9 +59,11 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +coverage.xml nosetests.xml htmlcov/ test-reports/ +test-results.xml # Translations *.mo diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5297fd802318d1..0ee272f900daa7 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -114,6 +114,7 @@ stages: - script: | . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests + script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) - script: | @@ -122,6 +123,7 @@ stages: . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) + script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - task: PublishTestResults@2 diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index f7e2f865cb2af0..e4f2033c3cbbeb 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -192,7 +192,8 @@ async def test_state_off_is_set(hass): """Test that state is set to off.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_OFF) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_OFF) assert hass.states.get(mock_entity_id).state == STATE_OFF @@ -217,7 +218,8 @@ async def test_state_idle_is_set(hass): """Test that state is set to idle.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_IDLE) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_IDLE) assert hass.states.get(mock_entity_id).state == STATE_IDLE @@ -246,7 +248,6 @@ async def test_media_attributes_are_fetched(hass): with patch(mock_func, return_value=mock_coro(mock_result)) as mock_fetch, patch( MOCK_SAVE, side_effect=MagicMock() ): - await mock_ddp_response(hass, MOCK_STATUS_PLAYING) mock_state = hass.states.get(mock_entity_id) @@ -271,7 +272,9 @@ async def test_media_attributes_are_loaded(hass): "pyps4.Ps4Async.async_get_ps_store_data", ) - with patch(mock_func, return_value=mock_coro(None)) as mock_fetch: + with patch(mock_func, return_value=mock_coro(None)) as mock_fetch, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await mock_ddp_response(hass, MOCK_STATUS_PLAYING, mock_data) mock_state = hass.states.get(mock_entity_id) @@ -292,7 +295,9 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF): + with patch( + "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF + ), patch(MOCK_SAVE, side_effect=MagicMock()): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -374,7 +379,9 @@ async def test_turn_on(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.wakeup" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_on", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -390,7 +397,9 @@ async def test_turn_off(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.standby" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_off", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -406,7 +415,9 @@ async def test_media_pause(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_pause", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -422,7 +433,9 @@ async def test_media_stop(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_stop", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -443,7 +456,9 @@ async def test_select_source(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name. await hass.services.async_call( "media_player", @@ -467,7 +482,9 @@ async def test_select_source_caps(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name in caps. await hass.services.async_call( "media_player", @@ -494,7 +511,9 @@ async def test_select_source_id(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title ID. await hass.services.async_call( "media_player", @@ -513,7 +532,9 @@ async def test_ps4_send_command(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( DOMAIN, "send_command", {ATTR_ENTITY_ID: mock_entity_id, ATTR_COMMAND: "ps"} ) From ce54ae31712cdb72a8ec52bf2d8dd80e1198ea16 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 21 Aug 2019 19:58:03 +0200 Subject: [PATCH 109/213] Update PyEssent (#26115) --- homeassistant/components/essent/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index 41313cb44a918a..aeb3b48311eff4 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -2,7 +2,7 @@ "domain": "essent", "name": "Essent", "documentation": "https://www.home-assistant.io/components/essent", - "requirements": ["PyEssent==0.12"], + "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17a707dedf8ad5..e6172fe4fa67cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ Mastodon.py==1.4.6 OPi.GPIO==0.3.6 # homeassistant.components.essent -PyEssent==0.12 +PyEssent==0.13 # homeassistant.components.github PyGithub==1.43.5 From 95f660f0b4b8c225a5c07c04a176f7f3da496a6c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 21 Aug 2019 19:58:34 +0200 Subject: [PATCH 110/213] remove ATTR_ID for groups (#26114) --- homeassistant/components/homematicip_cloud/binary_sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 7bb7718f0b3584..8ecbfeab01a17e 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_ID _LOGGER = logging.getLogger(__name__) @@ -311,6 +311,10 @@ def device_state_attributes(self): """Return the state attributes of the security zone group.""" attr = super().device_state_attributes + # Remove ATTR_ID from dict, because security groups don't have + # device id/sgtin, just an ugly uuid that is referenced no where else. + del attr[ATTR_ID] + if self._device.motionDetected: attr[ATTR_MOTIONDETECTED] = True if self._device.presenceDetected: From 7ab36e03813ad1c9c7594748da6afb0b299c7062 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Aug 2019 20:54:20 +0200 Subject: [PATCH 111/213] Update eternalegypt to 0.0.10 (#26117) --- CODEOWNERS | 1 + homeassistant/components/netgear_lte/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7a153b3ff82b0d..81c5aafed305a0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -180,6 +180,7 @@ homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff +homeassistant/components/netgear_lte/* @amelchio homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 8f5db991c76c9a..609ea72cc699c3 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,8 +3,10 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/components/netgear_lte", "requirements": [ - "eternalegypt==0.0.9" + "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@amelchio" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index e6172fe4fa67cf..1984b1bb478f93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -455,7 +455,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.9 +eternalegypt==0.0.10 # homeassistant.components.keyboard_remote # evdev==0.6.1 From 588eac82c725c7e03470f706392d80f1c410ffa6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 21 Aug 2019 22:22:42 +0200 Subject: [PATCH 112/213] UniFi config entry options (#26113) Introduce config entry options for Unifi integration Allow configuration.yaml options to be imported to new options --- .../components/unifi/.translations/en.json | 39 +++++++--- homeassistant/components/unifi/__init__.py | 10 +-- homeassistant/components/unifi/config_flow.py | 71 ++++++++++++++++- homeassistant/components/unifi/const.py | 13 +++- homeassistant/components/unifi/controller.py | 76 ++++++++++++++++++- .../components/unifi/device_tracker.py | 36 +++------ homeassistant/components/unifi/strings.json | 15 ++++ homeassistant/components/unifi/switch.py | 2 +- tests/components/unifi/test_controller.py | 19 ++++- tests/components/unifi/test_device_tracker.py | 24 +++--- tests/components/unifi/test_init.py | 3 +- tests/components/unifi/test_switch.py | 13 ++-- 12 files changed, 248 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 3686148fdb6452..c484bfbf09fa5d 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,26 +1,41 @@ { "config": { - "abort": { - "already_configured": "Controller site is already configured", - "user_privilege": "User needs to be administrator" - }, - "error": { - "faulty_credentials": "Bad user credentials", - "service_unavailable": "No service available" - }, + "title": "UniFi Controller", "step": { "user": { + "title": "Set up UniFi Controller", "data": { "host": "Host", + "username": "User name", "password": "Password", "port": "Port", "site": "Site ID", - "username": "User name", "verify_ssl": "Controller using proper certificate" - }, - "title": "Set up UniFi Controller" + } } }, - "title": "UniFi Controller" + "error": { + "faulty_credentials": "Bad user credentials", + "service_unavailable": "No service available" + }, + "abort": { + "already_configured": "Controller site is already configured", + "user_privilege": "User needs to be administrator" + } + }, + "options": { + "step": { + "init": { + "data": {} + }, + "device_tracker": { + "data": { + "detection_time": "Time in seconds from last seen until considered away", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)", + "track_wired_clients": "Include wired network clients" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4ca6f68c301f60..da9bbb8e59e986 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,9 +11,6 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -23,6 +20,9 @@ from .controller import UniFiController CONF_CONTROLLERS = "controllers" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONTROLLER_SCHEMA = vol.Schema( { @@ -34,9 +34,7 @@ vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, - vol.Optional(CONF_DETECTION_TIME): vol.All( - cv.time_period, cv.positive_timedelta - ), + vol.Optional(CONF_DETECTION_TIME): cv.positive_int, vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string]), } ) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e5a8965dff9d65..e1f0a91c774eb1 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,6 +2,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,7 +11,20 @@ CONF_VERIFY_SSL, ) -from .const import CONF_CONTROLLER, CONF_SITE_ID, DOMAIN, LOGGER +from .const import ( + CONF_CONTROLLER, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, + CONF_DETECTION_TIME, + CONF_SITE_ID, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DOMAIN, + LOGGER, +) from .controller import get_controller from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect @@ -26,6 +40,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return UnifiOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the UniFi flow.""" self.config = None @@ -142,3 +162,52 @@ async def async_step_import(self, import_config): self.desc = import_config[CONF_SITE_ID] return await self.async_step_user(user_input=config) + + +class UnifiOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Unifi options.""" + + def __init__(self, config_entry): + """Initialize UniFi options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the UniFi options.""" + return await self.async_step_device_tracker() + + async def async_step_device_tracker(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="device_tracker", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TRACK_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_WIRED_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_DEVICES, + default=self.config_entry.options.get( + CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES + ), + ): bool, + vol.Optional( + CONF_DETECTION_TIME, + default=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index b4864421cb9067..ffa9a28818bf55 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -13,9 +13,16 @@ CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" -CONF_DONT_TRACK_CLIENTS = "dont_track_clients" -CONF_DONT_TRACK_DEVICES = "dont_track_devices" -CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +CONF_TRACK_CLIENTS = "track_clients" +CONF_TRACK_DEVICES = "track_devices" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" +DEFAULT_BLOCK_CLIENTS = [] +DEFAULT_TRACK_CLIENTS = True +DEFAULT_TRACK_DEVICES = True +DEFAULT_TRACK_WIRED_CLIENTS = True +DEFAULT_DETECTION_TIME = 300 +DEFAULT_SSID_FILTER = [] + ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index cb82e6cf1c1d04..47c692b12b24a6 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,4 +1,6 @@ """UniFi Controller abstraction.""" +from datetime import timedelta + import asyncio import ssl import async_timeout @@ -15,8 +17,19 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, + CONF_DETECTION_TIME, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, CONF_SITE_ID, + CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_BLOCK_CLIENTS, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, LOGGER, UNIFI_CONFIG, ) @@ -59,9 +72,40 @@ def site_role(self): return self._site_role @property - def block_clients(self): - """Return list of clients to block.""" - return self.unifi_config.get(CONF_BLOCK_CLIENT, []) + def option_block_clients(self): + """Config entry option with list of clients to control network access.""" + return self.config_entry.options.get(CONF_BLOCK_CLIENT, DEFAULT_BLOCK_CLIENTS) + + @property + def option_track_clients(self): + """Config entry option to not track clients.""" + return self.config_entry.options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS) + + @property + def option_track_devices(self): + """Config entry option to not track devices.""" + return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) + + @property + def option_track_wired_clients(self): + """Config entry option to not track wired clients.""" + return self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + + @property + def option_detection_time(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) + + @property + def option_ssid_filter(self): + """Config entry option listing what SSIDs are being used to track clients.""" + return self.config_entry.options.get(CONF_SSID_FILTER, DEFAULT_SSID_FILTER) @property def mac(self): @@ -96,7 +140,7 @@ async def async_update(self): with async_timeout.timeout(10): await self.api.clients.update() await self.api.devices.update() - if self.block_clients: + if self.option_block_clients: await self.api.clients_all.update() except aiounifi.LoginRequired: @@ -155,6 +199,30 @@ async def async_setup(self): self.unifi_config = unifi_config break + options = dict(self.config_entry.options) + + if CONF_BLOCK_CLIENT in self.unifi_config: + options[CONF_BLOCK_CLIENT] = self.unifi_config[CONF_BLOCK_CLIENT] + + if CONF_TRACK_CLIENTS in self.unifi_config: + options[CONF_TRACK_CLIENTS] = self.unifi_config[CONF_TRACK_CLIENTS] + + if CONF_TRACK_DEVICES in self.unifi_config: + options[CONF_TRACK_DEVICES] = self.unifi_config[CONF_TRACK_DEVICES] + + if CONF_TRACK_WIRED_CLIENTS in self.unifi_config: + options[CONF_TRACK_WIRED_CLIENTS] = self.unifi_config[ + CONF_TRACK_WIRED_CLIENTS + ] + + if CONF_DETECTION_TIME in self.unifi_config: + options[CONF_DETECTION_TIME] = self.unifi_config[CONF_DETECTION_TIME] + + if CONF_SSID_FILTER in self.unifi_config: + options[CONF_SSID_FILTER] = self.unifi_config[CONF_SSID_FILTER] + + hass.config_entries.async_update_entry(self.config_entry, options=options) + for platform in ["device_tracker", "switch"]: hass.async_create_task( hass.config_entries.async_forward_entry_setup( diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 491a032e1cccf7..c8024808e390e7 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,12 +27,7 @@ from .const import ( ATTR_MANUFACTURER, CONF_CONTROLLER, - CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DOMAIN as UNIFI_DOMAIN, ) @@ -151,11 +146,11 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - if not controller.unifi_config.get(CONF_DONT_TRACK_CLIENTS, False): + if controller.option_track_clients: for client_id in controller.api.clients: - if client_id in tracked: + if client_id in tracked and tracked[client_id].entity_id: LOGGER.debug( "Updating UniFi tracked client %s (%s)", tracked[client_id].entity_id, @@ -168,15 +163,12 @@ def update_items(controller, async_add_entities, tracked): if ( not client.is_wired - and CONF_SSID_FILTER in controller.unifi_config - and client.essid not in controller.unifi_config[CONF_SSID_FILTER] + and controller.option_ssid_filter + and client.essid not in controller.option_ssid_filter ): continue - if ( - controller.unifi_config.get(CONF_DONT_TRACK_WIRED_CLIENTS, False) - and client.is_wired - ): + if not controller.option_track_wired_clients and client.is_wired: continue tracked[client_id] = UniFiClientTracker(client, controller) @@ -187,11 +179,11 @@ def update_items(controller, async_add_entities, tracked): client.mac, ) - if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): + if controller.option_track_devices: for device_id in controller.api.devices: - if device_id in tracked: + if device_id in tracked and tracked[device_id].entity_id: LOGGER.debug( "Updating UniFi tracked device %s (%s)", tracked[device_id].entity_id, @@ -229,14 +221,11 @@ async def async_update(self): @property def is_connected(self): """Return true if the client is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < detection_time: + ) < self.controller.option_detection_time: return True + return False @property @@ -291,15 +280,12 @@ async def async_update(self): @property def is_connected(self): """Return true if the device is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if self.device.state == 1 and ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) - < detection_time + < self.controller.option_detection_time ): return True + return False @property diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 938ac058d22a66..c484bfbf09fa5d 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -22,5 +22,20 @@ "already_configured": "Controller site is already configured", "user_privilege": "User needs to be administrator" } + }, + "options": { + "step": { + "init": { + "data": {} + }, + "device_tracker": { + "data": { + "detection_time": "Time in seconds from last seen until considered away", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)", + "track_wired_clients": "Include wired network clients" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 2b7965d1095b13..b7bb9b730ada2f 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -74,7 +74,7 @@ def update_items(controller, async_add_entities, switches, switches_off): devices = controller.api.devices # block client - for client_id in controller.block_clients: + for client_id in controller.option_block_clients: block_client_id = "block-{}".format(client_id) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index f92eebabd20107..714db8604b278b 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -37,9 +37,23 @@ async def test_controller_setup(): """Successful setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = { + UNIFI_CONFIG: [ + { + CONF_HOST: CONTROLLER_DATA[CONF_HOST], + CONF_SITE_ID: "nice name", + controller.CONF_BLOCK_CLIENT: [], + controller.CONF_TRACK_CLIENTS: True, + controller.CONF_TRACK_DEVICES: True, + controller.CONF_TRACK_WIRED_CLIENTS: True, + controller.CONF_DETECTION_TIME: 300, + controller.CONF_SSID_FILTER: [], + } + ] + } entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) @@ -89,6 +103,7 @@ async def test_controller_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "1.2.3.4" client.mac = "00:11:22:33:44:55" @@ -111,6 +126,7 @@ async def test_controller_no_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "5.6.7.8" api = Mock() @@ -182,6 +198,7 @@ async def test_reset_unloads_entry_if_setup(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 0da72c924c4e9a..30c2191625e10c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -14,6 +14,7 @@ from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, + CONF_SSID_FILTER, UNIFI_CONFIG, ) from homeassistant.const import ( @@ -133,7 +134,7 @@ async def mock_request(method, path, **kwargs): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -146,6 +147,7 @@ async def setup_controller(hass, mock_controller): config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -182,9 +184,9 @@ async def test_tracked_devices(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) - mock_controller.unifi_config = {unifi_dt.CONF_SSID_FILTER: ["ssid"]} + options = {CONF_SSID_FILTER: ["ssid"]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 5 @@ -234,7 +236,7 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append({}) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: True} + options = {unifi.CONF_BLOCK_CLIENT: True} config_entry = config_entries.ConfigEntry( 1, @@ -263,7 +265,7 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 4 @@ -275,9 +277,9 @@ async def test_dont_track_clients(hass, mock_controller): """Test dont track clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -293,9 +295,9 @@ async def test_dont_track_devices(hass, mock_controller): """Test dont track devices config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_DEVICES: True} + options = {unifi.controller.CONF_TRACK_DEVICES: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -311,9 +313,9 @@ async def test_dont_track_wired_clients(hass, mock_controller): """Test dont track wired clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) mock_controller.mock_device_responses.append({}) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index c7f87579c08e87..b725e34f61dc65 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,5 +1,4 @@ """Test UniFi setup process.""" -from datetime import timedelta from unittest.mock import Mock, patch from homeassistant.components import unifi @@ -44,7 +43,7 @@ async def test_setup_with_config(hass): unifi.CONF_HOST: "1.2.3.4", unifi.CONF_SITE_ID: "My site", unifi.CONF_BLOCK_CLIENT: ["12:34:56:78:90:AB"], - unifi.CONF_DETECTION_TIME: timedelta(seconds=3), + unifi.CONF_DETECTION_TIME: 3, unifi.CONF_SSID_FILTER: ["ssid"], } ] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 05c58abbc9442c..3ac9ddb17dc61b 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -250,7 +250,7 @@ async def mock_request(method, path, **kwargs): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -263,6 +263,7 @@ async def setup_controller(hass, mock_controller): config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -320,11 +321,9 @@ async def test_switches(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1]) - mock_controller.unifi_config = { - unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]] - } + options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 5 @@ -467,7 +466,7 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} + options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} config_entry = config_entries.ConfigEntry( 1, @@ -496,7 +495,7 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 3 From 9bcb48985be9d9498ed37b1c54cd2b57cc8ca307 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Wed, 21 Aug 2019 22:07:27 +0100 Subject: [PATCH 113/213] Template binary sensor attributes (#22664) * Added attribute support to template binary sensor with tests Added attribute support to template binary sensor with tests * fix dictionary update fix dictionary update * Fixed whitespace and line length issues * Fixed indentation * Simplify applying of attribute templates based on feedback * Syntax and whitespace fixes * Black formatting * Black formatting on tests * Check attribute_templates is not None * Fixed test * Added test for failure to render template * Test fix * Updated test * Removed whitespace and applied Black formatting * Fixed test assertion * Updated test * Code improvements folloing comments Using chain to iterate over templates and attribute_templates Replacing dict() with {} Rmoving unused constant * Applied Black formatting * Fixed removed code * Default attribute_templates to empty dict * Black formatting * Fixed imports --- .../components/template/binary_sensor.py | 46 ++++++++--- .../components/template/test_binary_sensor.py | 82 ++++++++++++++++++- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 8b354f4eeb2b57..e0fc867720010c 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,5 +1,6 @@ """Support for exposing a templated binary sensor.""" import logging +from itertools import chain import voluptuous as vol @@ -30,12 +31,14 @@ CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -59,14 +62,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, value_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - ): + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -78,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) + invalid_templates.append(tpl_name.replace("_template", "")) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -114,6 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, delay_on, delay_off, + attribute_templates, ) ) if not sensors: @@ -139,6 +146,7 @@ def __init__( entity_ids, delay_on, delay_off, + attribute_templates, ): """Initialize the Template binary sensor.""" self.hass = hass @@ -154,6 +162,8 @@ def __init__( self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -203,6 +213,11 @@ def device_class(self): """Return the sensor class of the sensor.""" return self._device_class + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -225,10 +240,21 @@ def _async_render(self): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - for property_name, template in ( - ("_icon", self._icon_template), - ("_entity_picture", self._entity_picture_template), - ): + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + } + + attrs = {} + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + try: + attrs[key] = value.async_render() + except TemplateError as err: + _LOGGER.error("Error rendering attribute %s: %s", key, err) + self._attributes = attrs + + for property_name, template in templates.items(): if template is None: continue diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c0b73f9c559623..c8cec168d6e6cd 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -166,6 +166,38 @@ def test_entity_picture_template(self): state = self.hass.states.get("binary_sensor.test_template_sensor") assert state.attributes["entity_picture"] == "/local/sensor.png" + def test_attribute_templates(self): + """Test attribute_templates template.""" + with assert_setup_component(1): + assert setup.setup_component( + self.hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes.get("test_attribute") == "It ." + + self.hass.states.set("sensor.test_state", "Works") + self.hass.block_till_done() + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes["test_attribute"] == "It Works." + @mock.patch( "homeassistant.components.template.binary_sensor." "BinarySensorTemplate._async_render" @@ -209,6 +241,7 @@ def test_attributes(self): MATCH_ALL, None, None, + None, ).result() assert not vs.should_poll assert "motion" == vs.device_class @@ -268,6 +301,7 @@ def test_update_template_error(self, mock_render): MATCH_ALL, None, None, + None, ).result() mock_render.side_effect = TemplateError("foo") run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() @@ -394,6 +428,36 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_invalid_attribute_template(hass, caplog): + """Test that errors are logged if rendering template fails.""" + hass.states.async_set("binary_sensor.test_sensor", "true") + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "invalid_template": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "attribute_templates": { + "test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}" + }, + } + }, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.invalid_template" + ) + + assert ("Error rendering attribute test_attribute") in caplog.text + + async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") @@ -414,12 +478,16 @@ async def test_no_update_template_match_all(hass, caplog): "value_template": "{{ states.binary_sensor.test_sensor.state }}", "entity_picture_template": "{{ 1 + 1 }}", }, + "all_attribute": { + "value_template": "{{ states.binary_sensor.test_sensor.state }}", + "attribute_templates": {"test_attribute": "{{ 1 + 1 }}"}, + }, }, } }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert ( "Template binary sensor all_state has no entity ids " "configured to track nor were we able to extract the entities to " @@ -435,10 +503,16 @@ async def test_no_update_template_match_all(hass, caplog): "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text + assert ( + "Template binary sensor all_attribute has no entity ids " + "configured to track nor were we able to extract the entities to " + "track from the test_attribute template" + ) in caplog.text assert hass.states.get("binary_sensor.all_state").state == "off" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -446,6 +520,7 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" hass.states.async_set("binary_sensor.test_sensor", "false") await hass.async_block_till_done() @@ -453,13 +528,18 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" await hass.helpers.entity_component.async_update_entity("binary_sensor.all_state") await hass.helpers.entity_component.async_update_entity("binary_sensor.all_icon") await hass.helpers.entity_component.async_update_entity( "binary_sensor.all_entity_picture" ) + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.all_attribute" + ) assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" From 709097043697794a46377690737db65ff9f2f271 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 21 Aug 2019 14:08:46 -0700 Subject: [PATCH 114/213] Add descriptive fields to script config (#26056) * Add descriptive fields to script config * Add script descriptions to hass.data["service_description_cache"] * Import SERVICE_DESCRIPTION_CACHE * Register script descriptions via async_set_service_schema * Add scripts test for loading and reloading service descriptions * Minor cleanup * Clean up script schema --- homeassistant/components/script/__init__.py | 23 +++++- homeassistant/helpers/service.py | 14 ++++ tests/components/script/test_init.py | 92 +++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 44e11d83afa146..d810d50cfbf629 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.script import Script @@ -31,6 +32,9 @@ ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" +CONF_DESCRIPTION = "description" +CONF_EXAMPLE = "example" +CONF_FIELDS = "fields" CONF_SEQUENCE = "sequence" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -38,7 +42,17 @@ GROUP_NAME_ALL_SCRIPTS = "all scripts" SCRIPT_ENTRY_SCHEMA = vol.Schema( - {CONF_ALIAS: cv.string, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA} + { + CONF_ALIAS: cv.string, + vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DESCRIPTION, default=""): cv.string, + vol.Optional(CONF_FIELDS, default={}): { + cv.string: { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_EXAMPLE): cv.string, + } + }, + } ) CONFIG_SCHEMA = vol.Schema( @@ -137,6 +151,13 @@ async def service_handler(service): DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA ) + # Register the service description + service_desc = { + CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], + CONF_FIELDS: cfg[CONF_FIELDS], + } + async_set_service_schema(hass, DOMAIN, object_id, service_desc) + await component.async_add_entities(scripts) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 07e070df8c5ec3..f29d1885d1e145 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -231,6 +231,20 @@ async def async_get_all_descriptions(hass): return descriptions +@ha.callback +@bind_hass +def async_set_service_schema(hass, domain, service, schema): + """Register a description for a service.""" + hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + + description = { + "description": schema.get("description") or "", + "fields": schema.get("fields") or {}, + } + + hass.data[SERVICE_DESCRIPTION_CACHE]["{}.{}".format(domain, service)] = description + + @bind_hass async def entity_service_call( hass, platforms, func, call, service_name="", required_features=None diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 7be682eff5e6a7..d675034e7442ae 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -17,6 +17,7 @@ EVENT_SCRIPT_STARTED, ) from homeassistant.core import Context, callback, split_entity_id +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component from homeassistant.exceptions import ServiceNotFound @@ -244,6 +245,61 @@ def test_reload_service(self): assert self.hass.services.has_service(script.DOMAIN, "test2") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: has "description" but no "fields" + assert await async_setup_component( + hass, + "script", + { + "script": { + "test": { + "description": "test description", + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[DOMAIN]["test"]["description"] == "test description" + assert not descriptions[DOMAIN]["test"]["fields"] + + # Test 2: has "fields" but no "description" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + with patch( + "homeassistant.config.load_yaml_config_file", + return_value={ + "script": { + "test": { + "fields": { + "test_param": { + "description": "test_param description", + "example": "test_param example", + } + }, + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[script.DOMAIN]["test"]["description"] == "" + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["description"] + == "test_param description" + ) + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["example"] + == "test_param example" + ) + + async def test_shared_context(hass): """Test that the shared context is passed down the chain.""" event = "test_event" @@ -306,3 +362,39 @@ async def test_turning_no_scripts_off(hass): await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {"entity_id": []}, blocking=True ) + + +async def test_async_get_descriptions_script(hass): + """Test async_set_service_schema for the script integration.""" + script = hass.components.script + script_config = { + script.DOMAIN: { + "test1": {"sequence": [{"service": "homeassistant.restart"}]}, + "test2": { + "description": "test2", + "fields": { + "param": { + "description": "param_description", + "example": "param_example", + } + }, + "sequence": [{"service": "homeassistant.restart"}], + }, + } + } + + await async_setup_component(hass, script.DOMAIN, script_config) + descriptions = await hass.helpers.service.async_get_all_descriptions() + + assert descriptions[script.DOMAIN]["test1"]["description"] == "" + assert not descriptions[script.DOMAIN]["test1"]["fields"] + + assert descriptions[script.DOMAIN]["test2"]["description"] == "test2" + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["description"] + == "param_description" + ) + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["example"] + == "param_example" + ) From 4a6f722b6d8c928c89fb2856028d5cade968745f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 15:16:34 -0700 Subject: [PATCH 115/213] Updated frontend to 20190821.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2337c3cb469217..648fc8b96dff09 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190820.1" + "home-assistant-frontend==20190821.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0dd9b6d7802629..b26e1c7e59faf6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1984b1bb478f93..33d6be841cf041 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24f383d81cc610..9d254f72e9bb38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190820.1 +home-assistant-frontend==20190821.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From da5860456a3354b0065026ba8dea0ca6af9e355b Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:19:50 +0200 Subject: [PATCH 116/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/manifest.json | 6 +- homeassistant/components/atome/sensor.py | 149 ++++--------------- 2 files changed, 30 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 0fa73277b06ac3..aeb1dc686a4c15 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,8 +1,8 @@ { "domain": "atome", "name": "Atome", - "documentation": "", + "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": [], - "requirements": [] + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.0.6"] } diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 750a129616cfe7..4a7f12034a08cf 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,11 +1,9 @@ """Linky Atome.""" import logging - from datetime import timedelta - -import pickle +from pyatome import AtomeClient import voluptuous as vol -import requests + from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,26 +12,17 @@ import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" DEFAULT_CLASS = "power" -ATOME_COOKIE = "atome_cookies.pickle" -ATOME_USER_ID = "atome_user_id.pickle" -ATOME_USER_REFERENCE = "atome_user_reference.pickle" SCAN_INTERVAL = timedelta(seconds=30) SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 -COOKIE_NAME = "PHPSESSID" -API_BASE_URI = "https://esoftlink.esoftthings.com" -API_ENDPOINT_LOGIN = "/api/user/login.json" -API_ENDPOINT_LIVE = "/measure/live.json" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -50,74 +39,50 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - timeout = config.get(CONF_TIMEOUT) - # # LOGIN - cookie_path = hass.config.path(ATOME_COOKIE) - user_id_path = hass.config.path(ATOME_USER_ID) - user_reference_path = hass.config.path(ATOME_USER_REFERENCE) + + """Initiate Atome Client object""" + try: + client = AtomeClient(username, password) + _LOGGER.debug("ATOME: CLIENT : user:%s, pass:%s, %s",username, password, pprint(client)) + + except PyAtomeError as exp: + _LOGGER.error(exp) + except Exception as exp: + _LOGGER.error(exp) + # finally: + # client.close_session() add_entities( [ AtomeSensor( name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, + client ) ] ) return True - -def load_file(filename): - """Load filename.""" - with open(filename, "rb") as file: - return pickle.load(file) - - -def save_file(content, filename): - """Save content to a file.""" - with open(filename, "wb") as file: - pickle.dump(content, file) - - class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" - def __init__( - self, - name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, - ): + def __init__(self,name,client: AtomeClient): + """Initialize the sensor.""" - _LOGGER.debug("ATOME: INIT") + _LOGGER.debug("ATOME: INIT : %s",str(client)) + _LOGGER.debug("ATOME: INIT : %s",pprint(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT self._device_class = DEFAULT_CLASS - self._username = username - self._password = password - self._timeout = timeout + self._client = client - self._cookie_path = cookie_path - self._user_id_path = user_id_path - self._user_reference_path = user_reference_path self._attributes = None self._state = None - # self.update = Throttle(SCAN_INTERVAL)(self._update) - # self.update() - self._login(username, password) + self._login() + self.get_live() @property def name(self): @@ -140,79 +105,19 @@ def state(self): return self._state # @Throttle(SESSION_RENEW_INTERVAL) - def _login(self, username, password): - - # Login the user into the Atome API. - payload = {"email": username, "plainPassword": password} - - req = requests.post( - API_BASE_URI + API_ENDPOINT_LOGIN, - json=payload, - headers={"content-type": "application/json"}, - timeout=self._timeout, - ) - response_json = req.json() - # _LOGGER.debug(response_json) - session_cookie = req.cookies.get(COOKIE_NAME) - - if session_cookie is None: - _LOGGER.exception("Login unsuccessful. Check your credentials") - return False - - user_id = str(response_json["id"]) - user_reference = response_json["subscriptions"][0]["reference"] - - # store cookie - save_file(session_cookie, self._cookie_path) - # store user id - save_file(user_id, self._user_id_path) - # store user ref - save_file(user_reference, self._user_reference_path) + def _login(self): - _LOGGER.info( - "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", - user_id, - user_reference, - ) - # /LOGIN - return user_id, user_reference + return self._client.login() - def _get_data(self, url): + def _get_data(self): - cookie = load_file(self._cookie_path) - cookies = {COOKIE_NAME: cookie} + return self._client.get_live() - req = requests.get(url, cookies=cookies, timeout=self._timeout) - values = req.json() - - if req.status_code == 302: - _LOGGER.warning("Unable to fetch Atome data: need to re-login! ") - - if req.status_code == 403: - self._login(self._username, self._password) - _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) - - if req.status_code != 200: - _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) - - return values @Throttle(SCAN_INTERVAL) def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = load_file(self._user_id_path) - user_reference = load_file(self._user_reference_path) - - url = ( - API_BASE_URI - + "/api/subscription/" - + user_id - + "/" - + user_reference - + API_ENDPOINT_LIVE - ) - - values = self._get_data(url) + values = self._get_data() self._state = values["last"] From 8130c11cb1901a6c36faa8270472b77b4f83c945 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:20:16 +0200 Subject: [PATCH 117/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 4a7f12034a08cf..529837655fe022 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv - + _LOGGER = logging.getLogger(__name__) @@ -22,8 +22,6 @@ SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - """Initiate Atome Client object""" try: client = AtomeClient(username, password) From f632252860b6e1d5d0f6a0051cc9ad2870f32d78 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:00 +0200 Subject: [PATCH 118/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 529837655fe022..d4e654e0918dea 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -10,7 +10,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) @@ -41,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Initiate Atome Client object""" try: client = AtomeClient(username, password) - _LOGGER.debug("ATOME: CLIENT : user:%s, pass:%s, %s",username, password, pprint(client)) - except PyAtomeError as exp: _LOGGER.error(exp) except Exception as exp: From 51b84249f1677b786d9547b9366f9aa933d9d33d Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:10 +0200 Subject: [PATCH 119/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index d4e654e0918dea..3acf7b7e30e016 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -64,7 +64,6 @@ def __init__(self,name,client: AtomeClient): """Initialize the sensor.""" _LOGGER.debug("ATOME: INIT : %s",str(client)) - _LOGGER.debug("ATOME: INIT : %s",pprint(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT From 459bf6c0ec4df1dc5447e6e7cc11e4a5e68383ce Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:59 +0200 Subject: [PATCH 120/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 3acf7b7e30e016..2a062f4c63a0c9 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -57,13 +57,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) return True + class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" - def __init__(self,name,client: AtomeClient): - + def __init__(self, name, client: AtomeClient): """Initialize the sensor.""" - _LOGGER.debug("ATOME: INIT : %s",str(client)) + _LOGGER.debug("ATOME: INIT : %s", str(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT @@ -71,7 +71,6 @@ def __init__(self,name,client: AtomeClient): self._client = client - self._attributes = None self._state = None self._login() @@ -106,7 +105,6 @@ def _get_data(self): return self._client.get_live() - @Throttle(SCAN_INTERVAL) def update(self): """Update device state.""" From 3526d507cc1699c62f851ef80bb416a091e6bd59 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:22:34 +0200 Subject: [PATCH 121/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 2a062f4c63a0c9..6694a62d21f59b 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -40,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Initiate Atome Client object""" try: client = AtomeClient(username, password) - except PyAtomeError as exp: - _LOGGER.error(exp) except Exception as exp: _LOGGER.error(exp) # finally: From a27866055b3617a6e0ca1f3c9ce4b8a92f8a795a Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:37:26 +0200 Subject: [PATCH 122/213] Wrong get data function --- homeassistant/components/atome/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 6694a62d21f59b..7906f24b28b6ea 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -72,7 +72,7 @@ def __init__(self, name, client: AtomeClient): self._attributes = None self._state = None self._login() - self.get_live() + self._get_data() @property def name(self): From 34e4e961ca7feaf31e4ffdf82d1913353271baf6 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:39:10 +0200 Subject: [PATCH 123/213] Do not deserver yet to be code owner --- homeassistant/components/atome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index aeb1dc686a4c15..cfbec478fd611b 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,6 +3,6 @@ "name": "Atome", "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": ["@baqs"], + "codeowners": [""], "requirements": ["pyatome==0.0.6"] } From f9e518d868cb112b866ee3256cd355aba31b3c49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 15:53:10 -0700 Subject: [PATCH 124/213] Fix tests leaving files behind (#26121) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 85e66ce829c5f2..5389954ca59578 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ desktop.ini # monkeytype monkeytype.sqlite3 + +# This is left behind by Azure Restore Cache +tmp_cache From 12f964ca57f2e56dbb4a9cfb8411a36ac14466f9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 Aug 2019 00:54:04 +0200 Subject: [PATCH 125/213] Statistics sensors repect given name (#26119) --- homeassistant/components/statistics/sensor.py | 5 +---- tests/components/statistics/test_sensor.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 252a29591c97db..51868c6d0a85f1 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -82,10 +82,7 @@ def __init__(self, entity_id, name, sampling_size, max_age, precision): """Initialize the Statistics sensor.""" self._entity_id = entity_id self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" - if not self.is_binary: - self._name = "{} {}".format(name, ATTR_MEAN) - else: - self._name = "{} {}".format(name, ATTR_COUNT) + self._name = name self._sampling_size = sampling_size self._max_age = max_age self._precision = precision diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 32aa2d56558df5..2a28876f5527b0 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -60,7 +60,7 @@ def test_binary_sensor_source(self): self.hass.states.set("binary_sensor.test_monitored", value) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_count") + state = self.hass.states.get("sensor.test") assert str(len(values)) == state.state @@ -87,7 +87,7 @@ def test_sensor_source(self): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state assert self.min == state.attributes.get("min_value") @@ -126,7 +126,7 @@ def test_sampling_size(self): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 3.8 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -155,7 +155,7 @@ def test_sampling_size_1(self): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") # require only one data point assert self.values[-1] == state.attributes.get("min_value") @@ -206,7 +206,7 @@ def mock_now(): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 6 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -248,7 +248,7 @@ def mock_now(): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert datetime( 2017, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC @@ -290,7 +290,7 @@ def test_initialize_from_database(self): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state @pytest.mark.skip("Flaky in CI") @@ -355,7 +355,7 @@ def mock_purge(self): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert expected_min_age == state.attributes.get("min_age") # The max_age timestamp should be 1 hour before what we have right From 9a16b7b0f661bf64076433d56133c56a7811918b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 08:58:41 +0200 Subject: [PATCH 126/213] Update azure-pipelines-release.yml for Azure Pipelines (#26128) * Update azure-pipelines-release.yml for Azure Pipelines * Update azure-pipelines-release.yml --- azure-pipelines-release.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 81bb1944bed2d0..d0cfc294db128f 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -7,7 +7,7 @@ trigger: pr: none variables: - name: versionBuilder - value: '5.2' + value: '6.1' - group: docker - group: github - group: twine @@ -155,48 +155,46 @@ stages: vmImage: 'ubuntu-latest' steps: - script: | - echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json - sudo service docker restart + mkdir -p ~/.docker + echo '{ "experimental": "enabled" }' > .docker/config.json - sleep 15 sudo docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Enable manifest / Docker login' - script: | set -e - export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { local tag_l=$1 local tag_r=$2 - sudo docker manifest create homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest create homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ homeassistant/i386-homeassistant:${tag_r} \ homeassistant/armhf-homeassistant:${tag_r} \ homeassistant/armv7-homeassistant:${tag_r} \ homeassistant/aarch64-homeassistant:${tag_r} - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/amd64-homeassistant:${tag_r} \ --os linux --arch amd64 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/i386-homeassistant:${tag_r} \ --os linux --arch i386 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armhf-homeassistant:${tag_r} \ --os linux --arch arm --variant=v6 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armv7-homeassistant:${tag_r} \ --os linux --arch arm --variant=v7 - sudo docker manifest annotate homeassistant/home-assistant:${tag_l} \ + sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/aarch64-homeassistant:${tag_r} \ --os linux --arch arm64 --variant=v8 - sudo docker manifest push --purge homeassistant/home-assistant:${tag_l} + sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} } # Create version tag From b3ae6a20ba12821cab366dac9fc44be7d66c455f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 09:28:46 +0200 Subject: [PATCH 127/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index d0cfc294db128f..7409be5f98cd4a 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -180,7 +180,7 @@ stages: sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/i386-homeassistant:${tag_r} \ - --os linux --arch i386 + --os linux --arch 386 sudo docker --config .docker manifest annotate homeassistant/home-assistant:${tag_l} \ homeassistant/armhf-homeassistant:${tag_r} \ From f793c71f5260d31619807dd9980b21a361bc9789 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:34:54 +0200 Subject: [PATCH 128/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 7409be5f98cd4a..44d910a8106903 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -197,6 +197,12 @@ stages: sudo docker --config .docker manifest push --purge homeassistant/home-assistant:${tag_l} } + sudo docker pull homeassistant/amd64-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/i368-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/armhf-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/armv7-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/aarch64-homeassistant:$(Build.SourceBranchName) + # Create version tag create_manifest "$(Build.SourceBranchName)" "$(Build.SourceBranchName)" @@ -205,6 +211,7 @@ stages: create_manifest "dev" "$(Build.SourceBranchName)" elif [[ "$version" =~ b ]]; then create_manifest "beta" "$(Build.SourceBranchName)" + create_manifest "rc" "$(Build.SourceBranchName)" else create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" From 2b78bfaf78ae225a7ca24f31e40ec6d67ca8d879 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:47:35 +0200 Subject: [PATCH 129/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 44d910a8106903..2ad13288e081fc 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -215,6 +215,7 @@ stages: else create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" + create_manifest "beta" "$(Build.SourceBranchName)" fi displayName: 'Create Meta-Image' From be0739626b4f3b9cb763fca2ff306b7e22a708d7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 22 Aug 2019 17:49:17 +0200 Subject: [PATCH 130/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 2ad13288e081fc..6b986329291877 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -216,6 +216,7 @@ stages: create_manifest "stable" "$(Build.SourceBranchName)" create_manifest "latest" "$(Build.SourceBranchName)" create_manifest "beta" "$(Build.SourceBranchName)" + create_manifest "rc" "$(Build.SourceBranchName)" fi displayName: 'Create Meta-Image' From bc5cec97f445ab108e4129a96ae932b5271fbadc Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 22 Aug 2019 18:00:15 +0200 Subject: [PATCH 131/213] Add myself as codeowner to HmIP Cloud (#26140) --- CODEOWNERS | 1 + homeassistant/components/homematicip_cloud/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 81c5aafed305a0..71520e11acfecc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homematic/* @pvizeli @danielperna84 +homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/honeywell/* @zxdavb homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index ee0d2cb1271bf3..2a041ce6689e77 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -7,5 +7,7 @@ "homematicip==0.10.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@SukramJ" + ] } From 82b1b10c28ac6ee126bc6eac3af2d1344fbdc9c8 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 22 Aug 2019 18:02:35 +0200 Subject: [PATCH 132/213] Splitt device_state_attributes between device and group for Homematic IP Cloud (#26137) * splitt device_state_attributes between device and group * readd device_state_attributes for access point --- .../components/homematicip_cloud/binary_sensor.py | 8 ++------ homeassistant/components/homematicip_cloud/device.py | 9 +++++---- homeassistant/components/homematicip_cloud/sensor.py | 6 ++++++ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 8ecbfeab01a17e..97746f3f472b43 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_ID +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -309,11 +309,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - attr = super().device_state_attributes - - # Remove ATTR_ID from dict, because security groups don't have - # device id/sgtin, just an ugly uuid that is referenced no where else. - del attr[ATTR_ID] + attr = {ATTR_MODEL_TYPE: self._device.modelType} if self._device.motionDetected: attr[ATTR_MOTIONDETECTED] = True diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0fffad8e97eff8..b086eaa29c75f7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -117,9 +117,10 @@ def icon(self) -> Optional[str]: def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = {} - for attr, attr_key in DEVICE_ATTRIBUTES.items(): - attr_value = getattr(self._device, attr, None) - if attr_value: - state_attr[attr_key] = attr_value + if isinstance(self._device, AsyncDevice): + for attr, attr_key in DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index add03c6b644605..c15b3121d3a63e 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -34,6 +34,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .device import ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -142,6 +143,11 @@ def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return "%" + @property + def device_state_attributes(self): + """Return the state attributes of the security zone group.""" + return {ATTR_MODEL_TYPE: self._device.modelType} + class HomematicipHeatingThermostat(HomematicipGenericDevice): """Representation of a HomematicIP heating thermostat device.""" From 2d432da14c06740763867d7b49c6ef8e79c3026f Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 22 Aug 2019 18:19:27 +0200 Subject: [PATCH 133/213] DuckDNS setup backoff (#25899) --- homeassistant/components/duckdns/__init__.py | 65 ++++++++-- tests/components/duckdns/test_init.py | 119 ++++++++++++++++--- 2 files changed, 153 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 7d677580177403..171d17faff9fac 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,13 +1,17 @@ """Integrate with DuckDNS.""" -from datetime import timedelta import logging +from asyncio import iscoroutinefunction +from datetime import timedelta import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN +from homeassistant.core import callback, CALLBACK_TYPE from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,25 +46,28 @@ async def async_setup(hass, config): token = config[DOMAIN][CONF_ACCESS_TOKEN] session = async_get_clientsession(hass) - result = await _update_duckdns(session, domain, token) - - if not result: - return False - - async def update_domain_interval(now): + async def update_domain_interval(_now): """Update the DuckDNS entry.""" - await _update_duckdns(session, domain, token) + return await _update_duckdns(session, domain, token) + + intervals = ( + INTERVAL, + timedelta(minutes=1), + timedelta(minutes=5), + timedelta(minutes=15), + timedelta(minutes=30), + ) + async_track_time_interval_backoff(hass, update_domain_interval, intervals) async def update_domain_service(call): """Update the DuckDNS entry.""" await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT]) - async_track_time_interval(hass, update_domain_interval, INTERVAL) hass.services.async_register( DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA ) - return result + return True _SENTINEL = object() @@ -89,3 +96,37 @@ async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False) return False return True + + +@callback +@bind_hass +def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: + """Add a listener that fires repetitively at every timedelta interval.""" + if not iscoroutinefunction: + _LOGGER.error("action needs to be a coroutine and return True/False") + return + + if not isinstance(intervals, (list, tuple)): + intervals = (intervals,) + remove = None + failed = 0 + + async def interval_listener(now): + """Handle elapsed intervals with backoff.""" + nonlocal failed, remove + try: + failed += 1 + if await action(now): + failed = 0 + finally: + delay = intervals[failed] if failed < len(intervals) else intervals[-1] + remove = async_track_point_in_utc_time(hass, interval_listener, now + delay) + + hass.async_run_job(interval_listener, dt_util.utcnow()) + + def remove_listener(): + """Remove interval listener.""" + if remove: + remove() # pylint: disable=not-callable + + return remove_listener diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py index 0fdfebac66e449..0213d9aefa6fd8 100644 --- a/tests/components/duckdns/test_init.py +++ b/tests/components/duckdns/test_init.py @@ -1,28 +1,29 @@ """Test the DuckDNS component.""" -import asyncio from datetime import timedelta - +import logging import pytest from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from homeassistant.components import duckdns from homeassistant.util.dt import utcnow +from homeassistant.components.duckdns import async_track_time_interval_backoff from tests.common import async_fire_time_changed DOMAIN = "bla" TOKEN = "abcdefgh" +_LOGGER = logging.getLogger(__name__) +INTERVAL = duckdns.INTERVAL @bind_hass -@asyncio.coroutine -def async_set_txt(hass, txt): +async def async_set_txt(hass, txt): """Set the txt record. Pass in None to remove it. This is a legacy helper method. Do not use it for new tests. """ - yield from hass.services.async_call( + await hass.services.async_call( duckdns.DOMAIN, duckdns.SERVICE_SET_TXT, {duckdns.ATTR_TXT: txt}, blocking=True ) @@ -41,40 +42,60 @@ def setup_duckdns(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" aioclient_mock.get( duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="OK" ) - result = yield from async_setup_component( + result = await async_setup_component( hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}} ) + + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_update_fails(hass, aioclient_mock): +async def test_setup_backoff(hass, aioclient_mock): """Test setup fails if first update fails.""" aioclient_mock.get( duckdns.UPDATE_URL, params={"domains": DOMAIN, "token": TOKEN}, text="KO" ) - result = yield from async_setup_component( + result = await async_setup_component( hass, duckdns.DOMAIN, {"duckdns": {"domain": DOMAIN, "access_token": TOKEN}} ) - assert not result + assert result + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 + # Copy of the DuckDNS intervals from duckdns/__init__.py + intervals = ( + INTERVAL, + timedelta(minutes=1), + timedelta(minutes=5), + timedelta(minutes=15), + timedelta(minutes=30), + ) + tme = utcnow() + await hass.async_block_till_done() + + _LOGGER.debug("Backoff...") + for idx in range(1, len(intervals)): + tme += intervals[idx] + async_fire_time_changed(hass, tme) + await hass.async_block_till_done() -@asyncio.coroutine -def test_service_set_txt(hass, aioclient_mock, setup_duckdns): + assert aioclient_mock.call_count == idx + 1 + + +async def test_service_set_txt(hass, aioclient_mock, setup_duckdns): """Test set txt service call.""" # Empty the fixture mock requests aioclient_mock.clear_requests() @@ -86,12 +107,11 @@ def test_service_set_txt(hass, aioclient_mock, setup_duckdns): ) assert aioclient_mock.call_count == 0 - yield from async_set_txt(hass, "some-txt") + await async_set_txt(hass, "some-txt") assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_service_clear_txt(hass, aioclient_mock, setup_duckdns): +async def test_service_clear_txt(hass, aioclient_mock, setup_duckdns): """Test clear txt service call.""" # Empty the fixture mock requests aioclient_mock.clear_requests() @@ -103,5 +123,66 @@ def test_service_clear_txt(hass, aioclient_mock, setup_duckdns): ) assert aioclient_mock.call_count == 0 - yield from async_set_txt(hass, None) + await async_set_txt(hass, None) assert aioclient_mock.call_count == 1 + + +async def test_async_track_time_interval_backoff(hass): + """Test setup fails if first update fails.""" + ret_val = False + call_count = 0 + tme = None + + async def _return(now): + nonlocal call_count, ret_val, tme + if tme is None: + tme = now + call_count += 1 + return ret_val + + intervals = ( + INTERVAL, + INTERVAL * 2, + INTERVAL * 5, + INTERVAL * 9, + INTERVAL * 10, + INTERVAL * 11, + INTERVAL * 12, + ) + + async_track_time_interval_backoff(hass, _return, intervals) + await hass.async_block_till_done() + + assert call_count == 1 + + _LOGGER.debug("Backoff...") + for idx in range(1, len(intervals)): + tme += intervals[idx] + async_fire_time_changed(hass, tme) + await hass.async_block_till_done() + + assert call_count == idx + 1 + + _LOGGER.debug("Max backoff reached - intervals[-1]") + for _idx in range(1, 10): + tme += intervals[-1] + async_fire_time_changed(hass, tme) + await hass.async_block_till_done() + + assert call_count == idx + 1 + _idx + + _LOGGER.debug("Reset backoff") + call_count = 0 + ret_val = True + tme += intervals[-1] + async_fire_time_changed(hass, tme) + await hass.async_block_till_done() + assert call_count == 1 + + _LOGGER.debug("No backoff - intervals[0]") + for _idx in range(2, 10): + tme += intervals[0] + async_fire_time_changed(hass, tme) + await hass.async_block_till_done() + + assert call_count == _idx From a61a3675696b43c31aa2177e126f1eb82fc9829a Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 19:46:31 +0200 Subject: [PATCH 134/213] Added proper codeowner --- CODEOWNERS | 1 + homeassistant/components/atome/manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 07501e27f5e8f9..416a25a3f592ef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index cfbec478fd611b..6bbf47d993ce27 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,6 +3,6 @@ "name": "Atome", "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": [""], - "requirements": ["pyatome==0.0.6"] + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.0.7"] } From aff151c90a11d872ca5717300aee3044492b02e5 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 22 Aug 2019 11:01:56 -0700 Subject: [PATCH 135/213] Load user-provided descriptions for python_scripts (#26069) * Load user-provided descriptions for python_scripts * Import SERVICE_DESCRIPTION_CACHE * Use async_set_service_schema to register service descriptions * Add python_script tests for loading service descriptions * Use async/await in test --- .../components/python_script/__init__.py | 15 +++ tests/components/python_script/test_init.py | 100 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 788da6a8d64321..715c06aca43c82 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -9,8 +9,10 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename +from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -90,10 +92,23 @@ def python_script_service_handler(call): continue hass.services.remove(DOMAIN, existing_service) + # Load user-provided service descriptions from python_scripts/services.yaml + services_yaml = os.path.join(path, "services.yaml") + if os.path.exists(services_yaml): + services_dict = load_yaml(services_yaml) + else: + services_dict = {} + for fil in glob.iglob(os.path.join(path, "*.py")): name = os.path.splitext(os.path.basename(fil))[0] hass.services.register(DOMAIN, name, python_script_service_handler) + service_desc = { + "description": services_dict.get(name, {}).get("description", ""), + "fields": services_dict.get(name, {}).get("fields", {}), + } + async_set_service_schema(hass, DOMAIN, name, service_desc) + @bind_hass def execute_script(hass, name, data=None): diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index fcf1519d4c76a2..d7732c00f94fe1 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -3,8 +3,11 @@ import logging from unittest.mock import patch, mock_open +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import execute +from homeassistant.components.python_script import DOMAIN, execute, FOLDER + +from tests.common import patch_yaml_files @asyncio.coroutine @@ -289,6 +292,101 @@ def test_reload(hass): assert hass.services.has_service("python_script", "reload") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: no user-provided services.yaml file + scripts1 = [ + "/some/config/dir/python_scripts/hello.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions1 = ( + "hello:\n" + " description: Description of hello.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello.py.\n" + " example: 'This is a test of python_script.hello'" + ) + services_yaml1 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions1 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts1 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml1 + ): + await async_setup_component(hass, DOMAIN, {}) + + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello"]["description"] == "Description of hello.py." + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["description"] + == "Parameter used by hello.py." + ) + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello" + ) + + assert descriptions[DOMAIN]["world_beer"]["description"] == "" + assert bool(descriptions[DOMAIN]["world_beer"]["fields"]) is False + + # Test 2: user-provided services.yaml file + scripts2 = [ + "/some/config/dir/python_scripts/hello2.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions2 = ( + "hello2:\n" + " description: Description of hello2.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello2.py.\n" + " example: 'This is a test of python_script.hello2'" + ) + services_yaml2 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions2 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts2 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml2 + ): + await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello2"]["description"] == "Description of hello2.py." + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["description"] + == "Parameter used by hello2.py." + ) + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello2" + ) + + @asyncio.coroutine def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once.""" From bc17170f954c5fc12bee2ef121d4da6ee55fd013 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Thu, 22 Aug 2019 22:26:08 +0300 Subject: [PATCH 136/213] Fix tuya switch state (#26145) * bump tuyaha 0.0.3 * bump tuyaha 0.0.3 --- homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 57eb3f17584da5..8d47d8a0173bad 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,7 +3,7 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/components/tuya", "requirements": [ - "tuyaha==0.0.2" + "tuyaha==0.0.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 33d6be841cf041..3f0e9daa673c9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1854,7 +1854,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.2 +tuyaha==0.0.3 # homeassistant.components.twentemilieu twentemilieu==0.1.0 From bff5b00a09dcbe15c07ddcbce705c9487c0afcfb Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Thu, 22 Aug 2019 20:40:48 +0100 Subject: [PATCH 137/213] Nissanleaf login fix (#26139) * Upgrade to pycarwings2.9 per 25 July 2019 API change * Remove rest of location tracker. Fix get_status_from_update call. --- .../components/nissan_leaf/__init__.py | 87 ++++--------------- .../components/nissan_leaf/device_tracker.py | 46 ---------- .../components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 21 insertions(+), 116 deletions(-) delete mode 100644 homeassistant/components/nissan_leaf/device_tracker.py diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 409b4d382083e8..38b7018af6c6c2 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -24,14 +24,12 @@ DATA_LEAF = "nissan_leaf_data" DATA_BATTERY = "battery" -DATA_LOCATION = "location" DATA_CHARGING = "charging" DATA_PLUGGED_IN = "plugged_in" DATA_CLIMATE = "climate" DATA_RANGE_AC = "range_ac_on" DATA_RANGE_AC_OFF = "range_ac_off" -CONF_NCONNECT = "nissan_connect" CONF_INTERVAL = "update_interval" CONF_CHARGING_INTERVAL = "update_interval_charging" CONF_CLIMATE_INTERVAL = "update_interval_climate" @@ -61,7 +59,6 @@ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), - vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)) ), @@ -84,7 +81,7 @@ extra=vol.ALLOW_EXTRA, ) -LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor", "device_tracker"] +LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor"] SIGNAL_UPDATE_LEAF = "nissan_leaf_update" @@ -177,8 +174,7 @@ def setup_leaf(car_config): hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: - if component != "device_tracker" or car_config[CONF_NCONNECT]: - load_platform(hass, component, DOMAIN, {}, car_config) + load_platform(hass, component, DOMAIN, {}, car_config) async_track_point_in_utc_time( hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE @@ -209,24 +205,20 @@ def __init__(self, hass, leaf, car_config): self.hass = hass self.leaf = leaf self.car_config = car_config - self.nissan_connect = car_config[CONF_NCONNECT] self.force_miles = car_config[CONF_FORCE_MILES] self.data = {} self.data[DATA_CLIMATE] = False self.data[DATA_BATTERY] = 0 self.data[DATA_CHARGING] = False - self.data[DATA_LOCATION] = False self.data[DATA_RANGE_AC] = 0 self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False self.next_update = None self.last_check = None self.request_in_progress = False - # Timestamp of last successful response from battery, - # climate or location. + # Timestamp of last successful response from battery or climate. self.last_battery_response = None self.last_climate_response = None - self.last_location_response = None self._remove_listener = None async def async_update_data(self, now): @@ -334,20 +326,6 @@ async def async_refresh_data(self, now): except CarwingsError: _LOGGER.error("Error fetching climate info") - if self.nissan_connect: - try: - location_response = await self.async_get_location() - - if location_response is None: - _LOGGER.debug("Empty Location Response Received") - self.data[DATA_LOCATION] = None - else: - _LOGGER.debug("Location Response: %s", location_response.__dict__) - self.data[DATA_LOCATION] = location_response - self.last_location_response = utcnow() - except CarwingsError: - _LOGGER.error("Error fetching location info") - self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) @@ -364,19 +342,6 @@ async def async_get_battery(self): from pycarwings2 import CarwingsError try: - # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status - ) - - # Store the date from the nissan servers - start_date = self._extract_start_date(start_server_info) - if start_date is None: - _LOGGER.info("No start date from servers. Aborting") - return None - - _LOGGER.debug("Start server date=%s", start_date) - # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) request = await self.hass.async_add_executor_job(self.leaf.request_update) @@ -393,21 +358,30 @@ async def async_get_battery(self): ) await asyncio.sleep(PYCARWINGS2_SLEEP) - # Note leaf.get_status_from_update is always returning 0, so - # don't try to use it anymore. - server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status + # We don't use the response from get_status_from_update + # apart from knowing that the car has responded saying it + # has given the latest battery status to Nissan. + check_result_info = await self.hass.async_add_executor_job( + self.leaf.get_status_from_update, request ) - latest_date = self._extract_start_date(server_info) - _LOGGER.debug("Latest server date=%s", latest_date) - if latest_date is not None and latest_date != start_date: + if check_result_info is not None: + # Get the latest battery status from Nissan servers. + # This has the SOC in it. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info _LOGGER.debug( "%s attempts exceeded return latest data from server", MAX_RESPONSE_ATTEMPTS, ) + # Get the latest data from the nissan servers, even though + # it may be out of date, it's better than nothing. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status.") @@ -465,29 +439,6 @@ async def async_set_climate(self, toggle): _LOGGER.debug("Climate result not returned by Nissan servers") return False - async def async_get_location(self): - """Get location from Nissan servers.""" - request = await self.hass.async_add_executor_job(self.leaf.request_location) - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.debug( - "Location data not in yet. (%s) (%s). " "Waiting %s seconds", - self.leaf.vin, - attempt, - PYCARWINGS2_SLEEP, - ) - await asyncio.sleep(PYCARWINGS2_SLEEP) - - location_status = await self.hass.async_add_executor_job( - self.leaf.get_status_from_location, request - ) - - if location_status is not None: - _LOGGER.debug("Location_status=%s", location_status.__dict__) - break - - return location_status - class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py deleted file mode 100644 index 11d18ee5a8e7cd..00000000000000 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Support for tracking a Nissan Leaf.""" -import logging - -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.util import slugify - -from . import DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF - -_LOGGER = logging.getLogger(__name__) - -ICON_CAR = "mdi:car" - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Nissan Leaf tracker.""" - if discovery_info is None: - return False - - def see_vehicle(): - """Handle the reporting of the vehicle position.""" - for vin, datastore in hass.data[DATA_LEAF].items(): - host_name = datastore.leaf.nickname - dev_id = "nissan_leaf_{}".format(slugify(host_name)) - if not datastore.data[DATA_LOCATION]: - _LOGGER.debug("No position found for vehicle %s", vin) - return - _LOGGER.debug( - "Updating device_tracker for %s with position %s", - datastore.leaf.nickname, - datastore.data[DATA_LOCATION].__dict__, - ) - attrs = {"updated_on": datastore.last_location_response} - see( - dev_id=dev_id, - host_name=host_name, - gps=( - datastore.data[DATA_LOCATION].latitude, - datastore.data[DATA_LOCATION].longitude, - ), - attributes=attrs, - icon=ICON_CAR, - ) - - dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) - - return True diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index ab94c01b7c1272..70aaa112414beb 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nissan leaf", "documentation": "https://www.home-assistant.io/components/nissan_leaf", "requirements": [ - "pycarwings2==2.8" + "pycarwings2==2.9" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3f0e9daa673c9f..61c9a48430927d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1071,7 +1071,7 @@ pyblackbird==0.5 pybotvac==0.0.15 # homeassistant.components.nissan_leaf -pycarwings2==2.8 +pycarwings2==2.9 # homeassistant.components.cloudflare pycfdns==0.0.1 From b7b8edc858f7c2acf7ebb9d5b6ce43a47498ad0f Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 22:45:19 +0200 Subject: [PATCH 138/213] Added pyatome requirement for Atome new platform --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 397c7ebb59975d..e734a5bec1c8e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1052,6 +1052,9 @@ pyarlo==0.2.3 # homeassistant.components.netatmo pyatmo==2.2.1 +# homeassistant.components.atome +pyatome==0.0.7 + # homeassistant.components.apple_tv pyatv==0.3.12 From a3461d528058af29ab57031cf40244afce8d7298 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 23:03:55 +0200 Subject: [PATCH 139/213] Black reformatted --- homeassistant/components/atome/sensor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 7906f24b28b6ea..6bc26a95f09c06 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -45,14 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # finally: # client.close_session() - add_entities( - [ - AtomeSensor( - name, - client - ) - ] - ) + add_entities([AtomeSensor(name, client)]) return True From 93c577f955e716f665bddd16cd9aca8695ddb56e Mon Sep 17 00:00:00 2001 From: Pierre <3458055+BaQs@users.noreply.github.com> Date: Thu, 22 Aug 2019 23:08:07 +0200 Subject: [PATCH 140/213] Update .coveragerc Co-Authored-By: Paulus Schoutsen --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 07c86dd0894e19..296c5b12210403 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,7 +50,7 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - homeassistant/components/atome/sensor.py + homeassistant/components/atome/* homeassistant/components/asuswrt/device_tracker.py homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py From aa56b4dd30b459f9c45b62870e00dc8ee9904478 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 14:12:24 -0700 Subject: [PATCH 141/213] Log warning if disabled entities receive updates. (#26143) * Log warning if disabled entities receive updates. * Fix test * Always set entity ID on disabled entities --- homeassistant/helpers/entity.py | 13 ++++++++ homeassistant/helpers/entity_platform.py | 6 ++-- .../components/config/test_entity_registry.py | 19 ++++++++++-- tests/helpers/test_entity.py | 31 +++++++++++++++++-- tests/helpers/test_entity_platform.py | 2 +- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index aecdf45dde5f88..7de41415f080ec 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -99,6 +99,9 @@ class Entity: # If we reported if this entity was slow _slow_reported = False + # If we reported this entity is updated while disabled + _disabled_reported = False + # Protect for multiple updates _update_staged = False @@ -273,6 +276,16 @@ def async_write_ha_state(self): @callback def _async_write_ha_state(self): """Write the state to the state machine.""" + if self.registry_entry and self.registry_entry.disabled_by: + if not self._disabled_reported: + self._disabled_reported = True + _LOGGER.warning( + "Entity %s is incorrectly being triggered for updates while it is disabled. This is a bug in the %s integration.", + self.entity_id, + self.platform.platform_name, + ) + return + start = timer() attr = {} diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 74351ac50af89f..4a6a3038fd0c97 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -349,6 +349,9 @@ async def _async_add_entity( disabled_by=disabled_by, ) + entity.registry_entry = entry + entity.entity_id = entry.entity_id + if entry.disabled: self.logger.info( "Not adding entity %s because it's disabled", @@ -358,9 +361,6 @@ async def _async_add_entity( ) return - entity.registry_entry = entry - entity.entity_id = entry.entity_id - # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index f18abe9b0e2b0f..64328a0c8c5647 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -127,13 +127,13 @@ async def test_update_entity(hass, client): assert state is not None assert state.name == "before update" + # UPDATE NAME await client.send_json( { "id": 6, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "name": "after update", - "disabled_by": "user", } ) @@ -142,7 +142,7 @@ async def test_update_entity(hass, client): assert msg["result"] == { "config_entry_id": None, "device_id": None, - "disabled_by": "user", + "disabled_by": None, "platform": "test_platform", "entity_id": "test_domain.world", "name": "after update", @@ -151,11 +151,24 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state.name == "after update" + # UPDATE DISABLED_BY TO USER + await client.send_json( + { + "id": 7, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": "user", + } + ) + + msg = await client.receive_json() + assert registry.entities["test_domain.world"].disabled_by == "user" + # UPDATE DISABLED_BY TO NONE await client.send_json( { - "id": 7, + "id": 8, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "disabled_by": None, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 58f76d396c1def..94650592d8e1bb 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,13 +7,13 @@ import pytest -import homeassistant.helpers.entity as entity +from homeassistant.helpers import entity, entity_registry from homeassistant.core import Context from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS from homeassistant.config import DATA_CUSTOMIZE from homeassistant.helpers.entity_values import EntityValues -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry def test_generate_entity_id_requires_hass_or_ids(): @@ -499,3 +499,30 @@ async def test_set_context_expired(hass): assert hass.states.get("hello.world").context != context assert ent._context is None assert ent._context_set is None + + +async def test_warn_disabled(hass, caplog): + """Test we warn once if we write to a disabled entity.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert "Entity hello.world is incorrectly being triggered" in caplog.text + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert caplog.text == "" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 606a4c82096291..caf8bb702afe87 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -491,7 +491,7 @@ async def test_registry_respect_entity_disabled(hass): platform = MockEntityPlatform(hass) entity = MockEntity(unique_id="1234") await platform.async_add_entities([entity]) - assert entity.entity_id is None + assert entity.entity_id == "test_domain.world" assert hass.states.async_entity_ids() == [] From 2e548112841f0a24904964ed1b565093d3b207e4 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 00:20:10 +0200 Subject: [PATCH 142/213] first commit of atome component --- homeassistant/components/atome/__init__.py | 0 homeassistant/components/atome/manifest.json | 9 + homeassistant/components/atome/sensor.py | 249 +++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 homeassistant/components/atome/__init__.py create mode 100644 homeassistant/components/atome/manifest.json create mode 100644 homeassistant/components/atome/sensor.py diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json new file mode 100644 index 00000000000000..272f4e135f9bed --- /dev/null +++ b/homeassistant/components/atome/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "energy", + "name": "Atome", + "documentation": "", + "dependencies": [], + "codeowners": [], + "requirements": [], + "homeassistant":"0.96.0" +} diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py new file mode 100644 index 00000000000000..4dc386c132fb2b --- /dev/null +++ b/homeassistant/components/atome/sensor.py @@ -0,0 +1,249 @@ +""" Linky Atome """ +import logging + +_LOGGER = logging.getLogger(__name__) + + +from datetime import timedelta + +import pickle +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_TIMEOUT, + STATE_UNAVAILABLE, + CONF_NAME, +) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +import homeassistant.helpers.config_validation as cv + +import requests + +KILOWATT_HOUR = "kWh" +DEFAULT_NAME = "atome" +DEFAULT_UNIT = "W" + +ATOME_COOKIE = "atome_cookies.pickle" +ATOME_USER_ID = "atome_user_id.pickle" +ATOME_USER_REFERENCE = "atome_user_reference.pickle" +SCAN_INTERVAL = timedelta(seconds=30) +DEFAULT_TIMEOUT = 10 + + +COOKIE_NAME = "PHPSESSID" +API_BASE_URI = "https://esoftlink.esoftthings.com" +API_ENDPOINT_LOGIN = "/api/user/login.json" +API_ENDPOINT_LIVE = "/measure/live.json" + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Linky sensor.""" + name = config.get(CONF_NAME) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + timeout = config.get(CONF_TIMEOUT) + + # # LOGIN + cookie_path = hass.config.path(ATOME_COOKIE) + user_id_path = hass.config.path(ATOME_USER_ID) + user_reference_path = hass.config.path(ATOME_USER_REFERENCE) + + atome = AtomeSensor( + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ) + user_id, user_reference = atome._login(username, password) + # # Login the user into the Atome API. + # payload = {"email": username, + # "plainPassword": password} + + # req = requests.post(API_BASE_URI + API_ENDPOINT_LOGIN, json=payload, headers={"content-type":"application/json"}) + # response_json = req.json() + # # _LOGGER.debug(response_json) + # session_cookie = req.cookies.get(COOKIE_NAME) + # user_id = str(response_json['id']) + # user_reference = response_json['subscriptions'][0]['reference'] + + # if session_cookie is None: + # _LOGGER.exception("Login unsuccessful. Check your credentials") + # return False + + # # store cookie + # with open(cookie_path, 'wb') as f: + # pickle.dump(session_cookie, f) + # # store user id + # with open(user_id_path, 'wb') as f: + # pickle.dump(user_id, f) + # # store user ref + # with open(user_reference_path, 'wb') as f: + # pickle.dump(user_reference, f) + + # _LOGGER.info("Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference) + # # /LOGIN + + add_entities( + [ + AtomeSensor( + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ) + ] + ) + return True + + +class AtomeSensor(Entity): + """Representation of a sensor entity for Linky.""" + + def __init__( + self, + name, + username, + password, + timeout, + cookie_path, + user_id_path, + user_reference_path, + ): + """Initialize the sensor.""" + _LOGGER.debug("ATOME: INIT") + self._name = name + # self._unit = DEFAULT_UNIT + self._unit_of_measurement = DEFAULT_UNIT + + self._username = username + self._password = password + self._timeout = timeout + + self._cookie_path = cookie_path + self._user_id_path = user_id_path + self._user_reference_path = user_reference_path + + self._attributes = None + self._state = None + # self.update = Throttle(SCAN_INTERVAL)(self._update) + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name or DEFAULT_NAME + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def _load_file(self, filename): + with open(filename, "rb") as f: + return pickle.load(f) + + def _login(self, username, password): + + # Login the user into the Atome API. + payload = {"email": username, "plainPassword": password} + + req = requests.post( + API_BASE_URI + API_ENDPOINT_LOGIN, + json=payload, + headers={"content-type": "application/json"}, + timeout=self._timeout, + ) + response_json = req.json() + # _LOGGER.debug(response_json) + session_cookie = req.cookies.get(COOKIE_NAME) + user_id = str(response_json["id"]) + user_reference = response_json["subscriptions"][0]["reference"] + + if session_cookie is None: + _LOGGER.exception("Login unsuccessful. Check your credentials") + return False + + # store cookie + with open(self._cookie_path, "wb") as f: + pickle.dump(session_cookie, f) + # store user id + with open(self._user_id_path, "wb") as f: + pickle.dump(user_id, f) + # store user ref + with open(self._user_reference_path, "wb") as f: + pickle.dump(user_reference, f) + + _LOGGER.info( + "Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", + user_id, + user_reference, + ) + # /LOGIN + return user_id, user_reference + + def _get_data(self, url): + + cookie = self._load_file(self._cookie_path) + cookies = {COOKIE_NAME: cookie} + + req = requests.get(url, cookies=cookies, timeout=self._timeout) + values = req.json() + + if req.status_code == 302: + _LOGGER.warning("Unable to fetch Linky data: need to re-login! ") + + if req.status_code != 200: + _LOGGER.warning("Unable to fetch Linky data: %s %s ", req.status_code, url) + + return values + + @Throttle(SCAN_INTERVAL) + def update(self): + """Update device state.""" + _LOGGER.debug("ATOME: Starting update of Atome Data") + + user_id = self._load_file(self._user_id_path) + user_reference = self._load_file(self._user_reference_path) + + url = ( + API_BASE_URI + + "/api/subscription/" + + user_id + + "/" + + user_reference + + API_ENDPOINT_LIVE + ) + + values = self._get_data(url) + self._state = values["last"] + + # TODO + # getData + # login From 27d710a61459a71556888218dddcf26da7c974e2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 00:49:11 +0200 Subject: [PATCH 143/213] sanatizing + fixed some errors handling --- homeassistant/components/atome/sensor.py | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 4dc386c132fb2b..e1f852dd461e5f 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -64,16 +64,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): user_id_path = hass.config.path(ATOME_USER_ID) user_reference_path = hass.config.path(ATOME_USER_REFERENCE) - atome = AtomeSensor( - name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, - ) - user_id, user_reference = atome._login(username, password) + # atome = AtomeSensor( + # name, + # username, + # password, + # timeout, + # cookie_path, + # user_id_path, + # user_reference_path, + # ) + # user_id, user_reference = atome._login(username, password) # # Login the user into the Atome API. # payload = {"email": username, # "plainPassword": password} @@ -148,7 +148,8 @@ def __init__( self._attributes = None self._state = None # self.update = Throttle(SCAN_INTERVAL)(self._update) - self.update() + # self.update() + self._login(username, password) @property def name(self): @@ -183,13 +184,14 @@ def _login(self, username, password): response_json = req.json() # _LOGGER.debug(response_json) session_cookie = req.cookies.get(COOKIE_NAME) - user_id = str(response_json["id"]) - user_reference = response_json["subscriptions"][0]["reference"] if session_cookie is None: _LOGGER.exception("Login unsuccessful. Check your credentials") return False + user_id = str(response_json["id"]) + user_reference = response_json["subscriptions"][0]["reference"] + # store cookie with open(self._cookie_path, "wb") as f: pickle.dump(session_cookie, f) From ba51bf69ff355f4ff5b77b95161a0d0ae2580d80 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:01:07 +0200 Subject: [PATCH 144/213] fixed re-login --- homeassistant/components/atome/sensor.py | 53 ++++-------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index e1f852dd461e5f..f5b53c7ebe6d91 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -33,6 +33,7 @@ ATOME_USER_ID = "atome_user_id.pickle" ATOME_USER_REFERENCE = "atome_user_reference.pickle" SCAN_INTERVAL = timedelta(seconds=30) +SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 @@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Linky sensor.""" + """Set up the sensor.""" name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -64,44 +65,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): user_id_path = hass.config.path(ATOME_USER_ID) user_reference_path = hass.config.path(ATOME_USER_REFERENCE) - # atome = AtomeSensor( - # name, - # username, - # password, - # timeout, - # cookie_path, - # user_id_path, - # user_reference_path, - # ) - # user_id, user_reference = atome._login(username, password) - # # Login the user into the Atome API. - # payload = {"email": username, - # "plainPassword": password} - - # req = requests.post(API_BASE_URI + API_ENDPOINT_LOGIN, json=payload, headers={"content-type":"application/json"}) - # response_json = req.json() - # # _LOGGER.debug(response_json) - # session_cookie = req.cookies.get(COOKIE_NAME) - # user_id = str(response_json['id']) - # user_reference = response_json['subscriptions'][0]['reference'] - - # if session_cookie is None: - # _LOGGER.exception("Login unsuccessful. Check your credentials") - # return False - - # # store cookie - # with open(cookie_path, 'wb') as f: - # pickle.dump(session_cookie, f) - # # store user id - # with open(user_id_path, 'wb') as f: - # pickle.dump(user_id, f) - # # store user ref - # with open(user_reference_path, 'wb') as f: - # pickle.dump(user_reference, f) - - # _LOGGER.info("Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference) - # # /LOGIN - add_entities( [ AtomeSensor( @@ -119,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AtomeSensor(Entity): - """Representation of a sensor entity for Linky.""" + """Representation of a sensor entity for Atome.""" def __init__( self, @@ -170,6 +133,7 @@ def _load_file(self, filename): with open(filename, "rb") as f: return pickle.load(f) + # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): # Login the user into the Atome API. @@ -203,7 +167,7 @@ def _login(self, username, password): pickle.dump(user_reference, f) _LOGGER.info( - "Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", + "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", user_id, user_reference, ) @@ -218,11 +182,12 @@ def _get_data(self, url): req = requests.get(url, cookies=cookies, timeout=self._timeout) values = req.json() - if req.status_code == 302: - _LOGGER.warning("Unable to fetch Linky data: need to re-login! ") + if req.status_code == 403: + self._login(self._username, self._password) + _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) if req.status_code != 200: - _LOGGER.warning("Unable to fetch Linky data: %s %s ", req.status_code, url) + _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) return values From c8d12f1ad9c68c62cbb19b724a639e3d722376db Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:20:20 +0200 Subject: [PATCH 145/213] updated coverage for atom which relies on external API --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 1d861d69c1dfe1..b7eb7f543de76e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,6 +50,7 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* + homeassistant/components/atome/sensor.py homeassistant/components/asuswrt/device_tracker.py homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py From 3f5baf2cf8e43a7e2dd5f701f673ab76104c6205 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:20:42 +0200 Subject: [PATCH 146/213] Proper manifest for atome --- homeassistant/components/atome/manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 272f4e135f9bed..0fa73277b06ac3 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,9 +1,8 @@ { - "domain": "energy", + "domain": "atome", "name": "Atome", "documentation": "", "dependencies": [], "codeowners": [], - "requirements": [], - "homeassistant":"0.96.0" + "requirements": [] } From cca54be81a03e634b8b13d981e02cd5d0b22626b Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 11:21:02 +0200 Subject: [PATCH 147/213] Added default power class for Atome --- homeassistant/components/atome/sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f5b53c7ebe6d91..fe3042b282bd2a 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -25,9 +25,9 @@ import requests -KILOWATT_HOUR = "kWh" DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" +DEFAULT_CLASS = "power" ATOME_COOKIE = "atome_cookies.pickle" ATOME_USER_ID = "atome_user_id.pickle" @@ -99,6 +99,7 @@ def __init__( self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT + self._device_class = DEFAULT_CLASS self._username = username self._password = password @@ -124,6 +125,11 @@ def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the device class.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" @@ -182,6 +188,9 @@ def _get_data(self, url): req = requests.get(url, cookies=cookies, timeout=self._timeout) values = req.json() + if req.status_code == 302: + _LOGGER.warning("Unable to fetch Atome data: need to re-login! ") + if req.status_code == 403: self._login(self._username, self._password) _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) From d27f12797c8a5f2542ad84607d94c6cce1670ef3 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 15:30:43 +0200 Subject: [PATCH 148/213] flake8 rules are now respected --- homeassistant/components/atome/__init__.py | 1 + homeassistant/components/atome/sensor.py | 20 ++++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py index e69de29bb2d1d6..6f524606a817bf 100644 --- a/homeassistant/components/atome/__init__.py +++ b/homeassistant/components/atome/__init__.py @@ -0,0 +1 @@ +"""Support for Atome devices connected to a Linky Energy Meter.""" diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index fe3042b282bd2a..d5075bca2820bd 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,30 +1,22 @@ -""" Linky Atome """ +"""Linky Atome.""" import logging -_LOGGER = logging.getLogger(__name__) - - from datetime import timedelta import pickle import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout - -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_TIMEOUT, - STATE_UNAVAILABLE, - CONF_NAME, -) + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle - import homeassistant.helpers.config_validation as cv import requests + +_LOGGER = logging.getLogger(__name__) + DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" DEFAULT_CLASS = "power" From f5e891a5afec3e1f5ac845f567aba6fafabf1f66 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 16:45:25 +0200 Subject: [PATCH 149/213] some pylint fixes --- homeassistant/components/atome/sensor.py | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index d5075bca2820bd..f8f3092e0f8221 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -5,6 +5,7 @@ import pickle import voluptuous as vol +import requests from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,8 +13,6 @@ from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -import requests - _LOGGER = logging.getLogger(__name__) @@ -127,9 +126,9 @@ def state(self): """Return the state of the sensor.""" return self._state - def _load_file(self, filename): - with open(filename, "rb") as f: - return pickle.load(f) + def load_file(self, filename): + with open(filename, "rb") as file: + return pickle.load(file) # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -155,14 +154,14 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - with open(self._cookie_path, "wb") as f: - pickle.dump(session_cookie, f) + with open(self._cookie_path, "wb") as file: + pickle.dump(session_cookie, file) # store user id - with open(self._user_id_path, "wb") as f: - pickle.dump(user_id, f) + with open(self._user_id_path, "wb") as file: + pickle.dump(user_id, file) # store user ref - with open(self._user_reference_path, "wb") as f: - pickle.dump(user_reference, f) + with open(self._user_reference_path, "wb") as file: + pickle.dump(user_reference, file) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", @@ -174,7 +173,7 @@ def _login(self, username, password): def _get_data(self, url): - cookie = self._load_file(self._cookie_path) + cookie = self.load_file(self._cookie_path) cookies = {COOKIE_NAME: cookie} req = requests.get(url, cookies=cookies, timeout=self._timeout) @@ -197,8 +196,8 @@ def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = self._load_file(self._user_id_path) - user_reference = self._load_file(self._user_reference_path) + user_id = self.load_file(self._user_id_path) + user_reference = self.load_file(self._user_reference_path) url = ( API_BASE_URI @@ -211,7 +210,3 @@ def update(self): values = self._get_data(url) self._state = values["last"] - - # TODO - # getData - # login From 00ff1b7049697e25b7f68d30310eac0449347f8b Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 17:06:06 +0200 Subject: [PATCH 150/213] Again, some flake8 fixes... --- homeassistant/components/atome/sensor.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f8f3092e0f8221..08ad81de8b40e8 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,9 +127,15 @@ def state(self): return self._state def load_file(self, filename): + """Loads a file stored with pickle.""" with open(filename, "rb") as file: return pickle.load(file) + def save_file(self, content, filename): + """Saves a file stored with pickle.""" + with open(filename, "wb") as file: + pickle.dump(content, file) + # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -154,14 +160,11 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - with open(self._cookie_path, "wb") as file: - pickle.dump(session_cookie, file) + self.save_file(session_cookie, self._cookie_path) # store user id - with open(self._user_id_path, "wb") as file: - pickle.dump(user_id, file) + self.save_file(user_id, self._user_id_path) # store user ref - with open(self._user_reference_path, "wb") as file: - pickle.dump(user_reference, file) + self.save_file(user_reference, self._user_reference_path) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", From e3d0608e159177c24fde23da58f8eebeee2144ae Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 17:20:30 +0200 Subject: [PATCH 151/213] First line should be in imperative mood --- homeassistant/components/atome/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 08ad81de8b40e8..7fe52fd13a8bb8 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,12 +127,12 @@ def state(self): return self._state def load_file(self, filename): - """Loads a file stored with pickle.""" + """A function to load a file stored with pickle.""" with open(filename, "rb") as file: return pickle.load(file) def save_file(self, content, filename): - """Saves a file stored with pickle.""" + """A function to save a file stored with pickle.""" with open(filename, "wb") as file: pickle.dump(content, file) From 0a1658dd48d6a5aa9e5719eeb03109c0be187864 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:05:13 +0200 Subject: [PATCH 152/213] flake8 stupidity... --- homeassistant/components/atome/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 7fe52fd13a8bb8..62ced807dc374c 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -127,12 +127,12 @@ def state(self): return self._state def load_file(self, filename): - """A function to load a file stored with pickle.""" + """Load filename.""" with open(filename, "rb") as file: return pickle.load(file) def save_file(self, content, filename): - """A function to save a file stored with pickle.""" + """Save content to a file.""" with open(filename, "wb") as file: pickle.dump(content, file) From 4d37e4b08f6650810120b43a97f0d700cd987a5a Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:09:25 +0200 Subject: [PATCH 153/213] Flake8 fixes --- homeassistant/components/atome/sensor.py | 34 +++++++++++++----------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 62ced807dc374c..e8a8d104a8edfb 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -72,6 +72,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True +def load_file(filename): + """Load filename.""" + with open(filename, "rb") as file: + return pickle.load(file) + + +def save_file(content, filename): + """Save content to a file.""" + with open(filename, "wb") as file: + pickle.dump(content, file) + + class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" @@ -126,16 +138,6 @@ def state(self): """Return the state of the sensor.""" return self._state - def load_file(self, filename): - """Load filename.""" - with open(filename, "rb") as file: - return pickle.load(file) - - def save_file(self, content, filename): - """Save content to a file.""" - with open(filename, "wb") as file: - pickle.dump(content, file) - # @Throttle(SESSION_RENEW_INTERVAL) def _login(self, username, password): @@ -160,11 +162,11 @@ def _login(self, username, password): user_reference = response_json["subscriptions"][0]["reference"] # store cookie - self.save_file(session_cookie, self._cookie_path) + save_file(session_cookie, self._cookie_path) # store user id - self.save_file(user_id, self._user_id_path) + save_file(user_id, self._user_id_path) # store user ref - self.save_file(user_reference, self._user_reference_path) + save_file(user_reference, self._user_reference_path) _LOGGER.info( "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", @@ -176,7 +178,7 @@ def _login(self, username, password): def _get_data(self, url): - cookie = self.load_file(self._cookie_path) + cookie = load_file(self._cookie_path) cookies = {COOKIE_NAME: cookie} req = requests.get(url, cookies=cookies, timeout=self._timeout) @@ -199,8 +201,8 @@ def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = self.load_file(self._user_id_path) - user_reference = self.load_file(self._user_reference_path) + user_id = load_file(self._user_id_path) + user_reference = load_file(self._user_reference_path) url = ( API_BASE_URI From f5ff16c8259d5f0e62d0513b493e319571266bac Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2019 18:21:27 +0200 Subject: [PATCH 154/213] Just a test --- homeassistant/components/atome/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index e8a8d104a8edfb..750a129616cfe7 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "atome" From 2b7d658cda0fd14218c9d95eb42e3a12b4fa8507 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:19:50 +0200 Subject: [PATCH 155/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/manifest.json | 6 +- homeassistant/components/atome/sensor.py | 149 ++++--------------- 2 files changed, 30 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 0fa73277b06ac3..aeb1dc686a4c15 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,8 +1,8 @@ { "domain": "atome", "name": "Atome", - "documentation": "", + "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": [], - "requirements": [] + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.0.6"] } diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 750a129616cfe7..4a7f12034a08cf 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,11 +1,9 @@ """Linky Atome.""" import logging - from datetime import timedelta - -import pickle +from pyatome import AtomeClient import voluptuous as vol -import requests + from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,26 +12,17 @@ import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "atome" DEFAULT_UNIT = "W" DEFAULT_CLASS = "power" -ATOME_COOKIE = "atome_cookies.pickle" -ATOME_USER_ID = "atome_user_id.pickle" -ATOME_USER_REFERENCE = "atome_user_reference.pickle" SCAN_INTERVAL = timedelta(seconds=30) SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 -COOKIE_NAME = "PHPSESSID" -API_BASE_URI = "https://esoftlink.esoftthings.com" -API_ENDPOINT_LOGIN = "/api/user/login.json" -API_ENDPOINT_LIVE = "/measure/live.json" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -50,74 +39,50 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - timeout = config.get(CONF_TIMEOUT) - # # LOGIN - cookie_path = hass.config.path(ATOME_COOKIE) - user_id_path = hass.config.path(ATOME_USER_ID) - user_reference_path = hass.config.path(ATOME_USER_REFERENCE) + + """Initiate Atome Client object""" + try: + client = AtomeClient(username, password) + _LOGGER.debug("ATOME: CLIENT : user:%s, pass:%s, %s",username, password, pprint(client)) + + except PyAtomeError as exp: + _LOGGER.error(exp) + except Exception as exp: + _LOGGER.error(exp) + # finally: + # client.close_session() add_entities( [ AtomeSensor( name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, + client ) ] ) return True - -def load_file(filename): - """Load filename.""" - with open(filename, "rb") as file: - return pickle.load(file) - - -def save_file(content, filename): - """Save content to a file.""" - with open(filename, "wb") as file: - pickle.dump(content, file) - - class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" - def __init__( - self, - name, - username, - password, - timeout, - cookie_path, - user_id_path, - user_reference_path, - ): + def __init__(self,name,client: AtomeClient): + """Initialize the sensor.""" - _LOGGER.debug("ATOME: INIT") + _LOGGER.debug("ATOME: INIT : %s",str(client)) + _LOGGER.debug("ATOME: INIT : %s",pprint(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT self._device_class = DEFAULT_CLASS - self._username = username - self._password = password - self._timeout = timeout + self._client = client - self._cookie_path = cookie_path - self._user_id_path = user_id_path - self._user_reference_path = user_reference_path self._attributes = None self._state = None - # self.update = Throttle(SCAN_INTERVAL)(self._update) - # self.update() - self._login(username, password) + self._login() + self.get_live() @property def name(self): @@ -140,79 +105,19 @@ def state(self): return self._state # @Throttle(SESSION_RENEW_INTERVAL) - def _login(self, username, password): - - # Login the user into the Atome API. - payload = {"email": username, "plainPassword": password} - - req = requests.post( - API_BASE_URI + API_ENDPOINT_LOGIN, - json=payload, - headers={"content-type": "application/json"}, - timeout=self._timeout, - ) - response_json = req.json() - # _LOGGER.debug(response_json) - session_cookie = req.cookies.get(COOKIE_NAME) - - if session_cookie is None: - _LOGGER.exception("Login unsuccessful. Check your credentials") - return False - - user_id = str(response_json["id"]) - user_reference = response_json["subscriptions"][0]["reference"] - - # store cookie - save_file(session_cookie, self._cookie_path) - # store user id - save_file(user_id, self._user_id_path) - # store user ref - save_file(user_reference, self._user_reference_path) + def _login(self): - _LOGGER.info( - "ATOME: Successfully logged in to Atome API. User ID: [%s], User REF: [%s]", - user_id, - user_reference, - ) - # /LOGIN - return user_id, user_reference + return self._client.login() - def _get_data(self, url): + def _get_data(self): - cookie = load_file(self._cookie_path) - cookies = {COOKIE_NAME: cookie} + return self._client.get_live() - req = requests.get(url, cookies=cookies, timeout=self._timeout) - values = req.json() - - if req.status_code == 302: - _LOGGER.warning("Unable to fetch Atome data: need to re-login! ") - - if req.status_code == 403: - self._login(self._username, self._password) - _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) - - if req.status_code != 200: - _LOGGER.warning("Unable to fetch Atome data: %s %s ", req.status_code, url) - - return values @Throttle(SCAN_INTERVAL) def update(self): """Update device state.""" _LOGGER.debug("ATOME: Starting update of Atome Data") - user_id = load_file(self._user_id_path) - user_reference = load_file(self._user_reference_path) - - url = ( - API_BASE_URI - + "/api/subscription/" - + user_id - + "/" - + user_reference - + API_ENDPOINT_LIVE - ) - - values = self._get_data(url) + values = self._get_data() self._state = values["last"] From c762d999971ba9cff52d723f2ba93093c51c9bb7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:20:16 +0200 Subject: [PATCH 156/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 4a7f12034a08cf..529837655fe022 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv - + _LOGGER = logging.getLogger(__name__) @@ -22,8 +22,6 @@ SESSION_RENEW_INTERVAL = timedelta(minutes=55) DEFAULT_TIMEOUT = 10 - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - """Initiate Atome Client object""" try: client = AtomeClient(username, password) From 9971a674966a6a1da41015db61a04caae823e306 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:00 +0200 Subject: [PATCH 157/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 529837655fe022..d4e654e0918dea 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -10,7 +10,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) @@ -41,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Initiate Atome Client object""" try: client = AtomeClient(username, password) - _LOGGER.debug("ATOME: CLIENT : user:%s, pass:%s, %s",username, password, pprint(client)) - except PyAtomeError as exp: _LOGGER.error(exp) except Exception as exp: From 071749badc6e164a66553d8bf235e0053dd4dca7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:10 +0200 Subject: [PATCH 158/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index d4e654e0918dea..3acf7b7e30e016 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -64,7 +64,6 @@ def __init__(self,name,client: AtomeClient): """Initialize the sensor.""" _LOGGER.debug("ATOME: INIT : %s",str(client)) - _LOGGER.debug("ATOME: INIT : %s",pprint(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT From 0491cd907c9252fb99d3ec941988d1a2607cccce Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:21:59 +0200 Subject: [PATCH 159/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 3acf7b7e30e016..2a062f4c63a0c9 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -57,13 +57,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) return True + class AtomeSensor(Entity): """Representation of a sensor entity for Atome.""" - def __init__(self,name,client: AtomeClient): - + def __init__(self, name, client: AtomeClient): """Initialize the sensor.""" - _LOGGER.debug("ATOME: INIT : %s",str(client)) + _LOGGER.debug("ATOME: INIT : %s", str(client)) self._name = name # self._unit = DEFAULT_UNIT self._unit_of_measurement = DEFAULT_UNIT @@ -71,7 +71,6 @@ def __init__(self,name,client: AtomeClient): self._client = client - self._attributes = None self._state = None self._login() @@ -106,7 +105,6 @@ def _get_data(self): return self._client.get_live() - @Throttle(SCAN_INTERVAL) def update(self): """Update device state.""" From ce1ec8f5fa13fb004061c1a2fae00f94ac105237 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:22:34 +0200 Subject: [PATCH 160/213] Export external calls to PyAtome pypi package --- homeassistant/components/atome/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 2a062f4c63a0c9..6694a62d21f59b 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -40,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Initiate Atome Client object""" try: client = AtomeClient(username, password) - except PyAtomeError as exp: - _LOGGER.error(exp) except Exception as exp: _LOGGER.error(exp) # finally: From 70c5b831be680eb19fa2e9d96d1cef2cb26d0b8d Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:37:26 +0200 Subject: [PATCH 161/213] Wrong get data function --- homeassistant/components/atome/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 6694a62d21f59b..7906f24b28b6ea 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -72,7 +72,7 @@ def __init__(self, name, client: AtomeClient): self._attributes = None self._state = None self._login() - self.get_live() + self._get_data() @property def name(self): From 1f01cb12e0a77b9aac11c0d2aeeb3e411f3dc118 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 00:39:10 +0200 Subject: [PATCH 162/213] Do not deserver yet to be code owner --- homeassistant/components/atome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index aeb1dc686a4c15..cfbec478fd611b 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,6 +3,6 @@ "name": "Atome", "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": ["@baqs"], + "codeowners": [""], "requirements": ["pyatome==0.0.6"] } From e9f218ecb9c8a6b611332c49a905e681ca7f63e7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 19:46:31 +0200 Subject: [PATCH 163/213] Added proper codeowner --- CODEOWNERS | 1 + homeassistant/components/atome/manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 71520e11acfecc..ea6905366e184b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index cfbec478fd611b..6bbf47d993ce27 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,6 +3,6 @@ "name": "Atome", "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], - "codeowners": [""], - "requirements": ["pyatome==0.0.6"] + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.0.7"] } From 1f06a3cdd49258a0e1658bbf234c86115c730f9b Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 22:45:19 +0200 Subject: [PATCH 164/213] Added pyatome requirement for Atome new platform --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 61c9a48430927d..a7ce02e362b541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1055,6 +1055,9 @@ pyarlo==0.2.3 # homeassistant.components.netatmo pyatmo==2.2.1 +# homeassistant.components.atome +pyatome==0.0.7 + # homeassistant.components.apple_tv pyatv==0.3.12 From 11133499ded4449140bfcf32919f59c0c256af64 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 23:03:55 +0200 Subject: [PATCH 165/213] Black reformatted --- homeassistant/components/atome/sensor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 7906f24b28b6ea..6bc26a95f09c06 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -45,14 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # finally: # client.close_session() - add_entities( - [ - AtomeSensor( - name, - client - ) - ] - ) + add_entities([AtomeSensor(name, client)]) return True From 5d5361f0c259735ac76d886167fc93c0a995086a Mon Sep 17 00:00:00 2001 From: Pierre <3458055+BaQs@users.noreply.github.com> Date: Thu, 22 Aug 2019 23:08:07 +0200 Subject: [PATCH 166/213] Update .coveragerc Co-Authored-By: Paulus Schoutsen --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index b7eb7f543de76e..fa743f6649deb3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,7 +50,7 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - homeassistant/components/atome/sensor.py + homeassistant/components/atome/* homeassistant/components/asuswrt/device_tracker.py homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py From 3a96ac40a1ac0b97c7ba08c140226f14ea25807f Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 23:49:37 +0200 Subject: [PATCH 167/213] Logging --- homeassistant/components/atome/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 6bc26a95f09c06..95c2bb2cd517e9 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,8 +1,8 @@ """Linky Atome.""" -import logging from datetime import timedelta from pyatome import AtomeClient import voluptuous as vol +import logging from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_NAME From 6e326f56728bd7c9763f62eb16c49fa8320e1017 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 22 Aug 2019 23:49:45 +0200 Subject: [PATCH 168/213] New pyAtome version --- homeassistant/components/atome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 6bbf47d993ce27..b46eafbec1d8ee 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], "codeowners": ["@baqs"], - "requirements": ["pyatome==0.0.7"] + "requirements": ["pyatome==0.0.9"] } From a4eeaac24c65ad416eeb9d10ca7a510aa85a139f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 15:05:57 -0700 Subject: [PATCH 169/213] Updated frontend to 20190822.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 648fc8b96dff09..8d6271183bd87e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190821.0" + "home-assistant-frontend==20190822.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b26e1c7e59faf6..0f4fb56970ba78 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 61c9a48430927d..d3d60e6a43e66e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d254f72e9bb38..b5d139719ef6de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190821.0 +home-assistant-frontend==20190822.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 12aedb74bdbc1ecb3c24ab018075ca24ae821e9c Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 23 Aug 2019 00:12:59 +0200 Subject: [PATCH 170/213] PyAtome 0.0.11 is now necessary --- homeassistant/components/atome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index b46eafbec1d8ee..885f5179e9cb3e 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/atome", "dependencies": [], "codeowners": ["@baqs"], - "requirements": ["pyatome==0.0.9"] + "requirements": ["pyatome==0.0.11"] } From f704a8e90e7986cb9281fcd6d83a0b9b9e0f26d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Aug 2019 17:32:43 -0700 Subject: [PATCH 171/213] Reload config entry when entity enabled in entity registry, remove entity if disabled. (#26120) * Reload config entry when disabled_by updated in entity registry * Add types * Remove entities that get disabled * Remove unnecessary domain checks. * Attach handler in async_setup * Remove unused var * Type * Fix test * Fix tests --- homeassistant/config_entries.py | 116 ++++++++++++++++-- homeassistant/helpers/entity.py | 4 + homeassistant/helpers/entity_registry.py | 2 +- .../components/config/test_entity_registry.py | 1 + tests/helpers/test_entity.py | 31 +++++ tests/helpers/test_entity_registry.py | 1 + tests/test_config_entries.py | 76 ++++++++++++ 7 files changed, 219 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2e1fbea14d1392..c2da37943c1abb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,13 +3,7 @@ import logging import functools import uuid -from typing import ( - Any, - Callable, - List, - Optional, - Set, # noqa pylint: disable=unused-import -) +from typing import Any, Callable, List, Optional, Set import weakref import attr @@ -19,6 +13,7 @@ from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry +from homeassistant.helpers import entity_registry # mypy: allow-untyped-defs @@ -161,8 +156,6 @@ async def async_setup( try: component = integration.get_component() - if self.domain == integration.domain: - integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( "Error importing integration %s to set up %s config entry: %s", @@ -174,8 +167,20 @@ async def async_setup( self.state = ENTRY_STATE_SETUP_ERROR return - # Perform migration - if integration.domain == self.domain: + if self.domain == integration.domain: + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error importing platform config_flow from integration %s to set up %s config entry: %s", + integration.domain, + self.domain, + err, + ) + self.state = ENTRY_STATE_SETUP_ERROR + return + + # Perform migration if not await self.async_migrate(hass): self.state = ENTRY_STATE_MIGRATION_ERROR return @@ -383,6 +388,7 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + EntityRegistryDisabledHandler(hass).async_setup() @callback def async_domains(self) -> List[str]: @@ -757,3 +763,91 @@ def update(self, *, disable_new_entities): def as_dict(self): """Return dictionary version of this config entrys system options.""" return {"disable_new_entities": self.disable_new_entities} + + +class EntityRegistryDisabledHandler: + """Handler to handle when entities related to config entries updating disabled_by.""" + + RELOAD_AFTER_UPDATE_DELAY = 30 + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the handler.""" + self.hass = hass + self.registry: Optional[entity_registry.EntityRegistry] = None + self.changed: Set[str] = set() + self._remove_call_later: Optional[Callable[[], None]] = None + + @callback + def async_setup(self) -> None: + """Set up the disable handler.""" + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated + ) + + async def _handle_entry_updated(self, event): + """Handle entity registry entry update.""" + if ( + event.data["action"] != "update" + or "disabled_by" not in event.data["changes"] + ): + return + + if self.registry is None: + self.registry = await entity_registry.async_get_registry(self.hass) + + entity_entry = self.registry.async_get(event.data["entity_id"]) + + if ( + # Stop if no entry found + entity_entry is None + # Stop if entry not connected to config entry + or entity_entry.config_entry_id is None + # Stop if the entry got disabled. In that case the entity handles it + # themselves. + or entity_entry.disabled_by + ): + return + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + + if config_entry.entry_id not in self.changed and await support_entry_unload( + self.hass, config_entry.domain + ): + self.changed.add(config_entry.entry_id) + + if not self.changed: + return + + # We are going to delay reloading on *every* entity registry change so that + # if a user is happily clicking along, it will only reload at the end. + + if self._remove_call_later: + self._remove_call_later() + + self._remove_call_later = self.hass.helpers.event.async_call_later( + self.RELOAD_AFTER_UPDATE_DELAY, self._handle_reload + ) + + async def _handle_reload(self, _now): + """Handle a reload.""" + self._remove_call_later = None + to_reload = self.changed + self.changed = set() + + _LOGGER.info( + "Reloading config entries because disabled_by changed in entity registry: %s", + ", ".join(self.changed), + ) + + await asyncio.gather( + *[self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload] + ) + + +async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: + """Test if a domain supports entry unloading.""" + integration = await loader.async_get_integration(hass, domain) + component = integration.get_component() + return hasattr(component, "async_unload_entry") diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7de41415f080ec..bd96e1bafdb5f4 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -503,6 +503,10 @@ async def _async_registry_updated(self, event): old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) + if self.registry_entry.disabled_by is not None: + await self.async_remove() + return + if self.registry_entry.entity_id == old.entity_id: self.async_write_ha_state() return diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3d84313a5c650d..7d81f62fa1c051 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -302,7 +302,7 @@ def _async_update_entity( self.async_schedule_save() - data = {"action": "update", "entity_id": entity_id} + data = {"action": "update", "entity_id": entity_id, "changes": list(changes)} if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 64328a0c8c5647..9472d8882540c9 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -163,6 +163,7 @@ async def test_update_entity(hass, client): msg = await client.receive_json() + assert hass.states.get("test_domain.world") is None assert registry.entities["test_domain.world"].disabled_by == "user" # UPDATE DISABLED_BY TO NONE diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 94650592d8e1bb..3c89a5c65379d6 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -526,3 +526,34 @@ async def test_warn_disabled(hass, caplog): ent.async_write_ha_state() assert hass.states.get("hello.world") is None assert caplog.text == "" + + +async def test_disabled_in_entity_registry(hass): + """Test entity is removed if we disable entity registry entry.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + registry = mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + await ent.async_internal_added_to_hass() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + + entry2 = registry.async_update_entity("hello.world", disabled_by=None) + await hass.async_block_till_done() + assert entry2 != entry + assert ent.registry_entry == entry2 + + entry3 = registry.async_update_entity("hello.world", disabled_by="user") + await hass.async_block_till_done() + assert entry3 != entry2 + assert ent.registry_entry == entry3 diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index aee6b6f19a3965..9debbdbcba7cda 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -219,6 +219,7 @@ async def test_updating_config_entry_id(hass, registry, update_events): assert update_events[0]["entity_id"] == entry.entity_id assert update_events[1]["action"] == "update" assert update_events[1]["entity_id"] == entry.entity_id + assert update_events[1]["changes"] == ["config_entry_id"] async def test_removing_config_entry_id(hass, registry, update_events): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ca6872a7a2cc1e..d9dd614c9a5e4a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,6 +20,7 @@ MockEntity, mock_integration, mock_entity_platform, + mock_registry, ) @@ -925,3 +926,78 @@ async def test_init_custom_integration(hass): return_value=mock_coro(integration), ): await hass.config_entries.flow.async_init("bla") + + +async def test_support_entry_unload(hass): + """Test unloading entry.""" + assert await config_entries.support_entry_unload(hass, "light") + assert not await config_entries.support_entry_unload(hass, "auth") + + +async def test_reload_entry_entity_registry_ignores_no_entry(hass): + """Test reloading entry in entity registry skips if no config entry linked.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + registry = mock_registry(hass) + + # Test we ignore entities without config entry + entry = registry.async_get_or_create("light", "hue", "123") + registry.async_update_entity(entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + +async def test_reload_entry_entity_registry_works(hass): + """Test we schedule an entry to be reloaded if disabled_by is updated.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + handler.async_setup() + registry = mock_registry(hass) + + config_entry = MockConfigEntry( + domain="comp", state=config_entries.ENTRY_STATE_LOADED + ) + config_entry.add_to_hass(hass) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Only changing disabled_by should update trigger + entity_entry = registry.async_get_or_create( + "light", "hue", "123", config_entry=config_entry + ) + registry.async_update_entity(entity_entry.entity_id, name="yo") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Disable entity, we should not do anything, only act when enabled. + registry.async_update_entity(entity_entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Enable entity, check we are reloading config entry. + registry.async_update_entity(entity_entry.entity_id, disabled_by=None) + await hass.async_block_till_done() + assert handler.changed == {config_entry.entry_id} + assert handler._remove_call_later is not None + + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta( + seconds=config_entries.EntityRegistryDisabledHandler.RELOAD_AFTER_UPDATE_DELAY + + 1 + ), + ) + await hass.async_block_till_done() + + assert len(mock_unload_entry.mock_calls) == 1 From 432f6569ad9bbd545180afc0283ac0b674f8a755 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Fri, 23 Aug 2019 01:23:19 -0700 Subject: [PATCH 172/213] Venstar: define success for all branches of set_temperature() (#26148) * define success for all branches * add operation_mode to error when unexpected value * fix black linting * black linting * fix weird black linting result --- homeassistant/components/venstar/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index e75ee2387ee689..7e1ae1ecd60b09 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -265,9 +265,11 @@ def set_temperature(self, **kwargs): elif operation_mode == self._client.MODE_AUTO: success = self._client.set_setpoints(temp_low, temp_high) else: + success = False _LOGGER.error( "The thermostat is currently not in a mode " - "that supports target temperature" + "that supports target temperature: %s", + operation_mode, ) if not success: From 2b6c5eeb1df2b0bf9eeb65ff2869969077ddb130 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 23 Aug 2019 13:54:44 +0200 Subject: [PATCH 173/213] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 6b986329291877..2e537fbb774567 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -198,7 +198,7 @@ stages: } sudo docker pull homeassistant/amd64-homeassistant:$(Build.SourceBranchName) - sudo docker pull homeassistant/i368-homeassistant:$(Build.SourceBranchName) + sudo docker pull homeassistant/i386-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/armhf-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/armv7-homeassistant:$(Build.SourceBranchName) sudo docker pull homeassistant/aarch64-homeassistant:$(Build.SourceBranchName) From 17750a604ec6566ca46fd49abbbd6ddd10a60544 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Fri, 23 Aug 2019 08:13:06 -0400 Subject: [PATCH 174/213] Add NWS weather (#23647) * Add nws weather. * Hassfest * Address multiple comments * Add NWS icon weather code link * Add metar fallback. Use metar code from nws observation if normal api is missing data. * only get 1 observation - we dont use more than 1 * add mocked metar for tests * lint * mock metar package for all tests * add check for metar attributes * catch errors in setup * add timeout error * handle request exceptions * check and test for missing observations * refactor to new pynws * change to simpler api * Make py3.5 compatible Remove f string * bump pynws version * gen_requirements * fix wind bearing observation * Revert "Make py3.5 compatible" This reverts commit 4946d91779a6e539ea43e667b2265557a49a0bb5. * Precommit black missed a file? * black test * add exceptional weather condition * bump pynws version * update requirements_all * address comments * move observation and forecast outside try-except-else * Revert "move observation and forecast outside try-except-else" This reverts commit 53b78b32837b55b8a0b61de6192e846f6a486754. * remove else from update forecast block * remove unneeded ConfigEntryNotReady import * add scan_interval, reduce min_time_between_updates * pytest tests * lint test docstring * use async await * lat and lon inclusive in config --- CODEOWNERS | 1 + homeassistant/components/nws/__init__.py | 1 + homeassistant/components/nws/manifest.json | 8 + homeassistant/components/nws/weather.py | 378 +++++++ homeassistant/components/weather/__init__.py | 6 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/nws/test_weather.py | 274 +++++ tests/fixtures/nws-weather-fore-null.json | 80 ++ tests/fixtures/nws-weather-fore-valid.json | 80 ++ tests/fixtures/nws-weather-obs-null.json | 161 +++ tests/fixtures/nws-weather-obs-valid.json | 161 +++ tests/fixtures/nws-weather-sta-valid.json | 996 +++++++++++++++++++ 14 files changed, 2150 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/nws/__init__.py create mode 100644 homeassistant/components/nws/manifest.json create mode 100644 homeassistant/components/nws/weather.py create mode 100644 tests/components/nws/test_weather.py create mode 100644 tests/fixtures/nws-weather-fore-null.json create mode 100644 tests/fixtures/nws-weather-fore-valid.json create mode 100644 tests/fixtures/nws-weather-obs-null.json create mode 100644 tests/fixtures/nws-weather-obs-valid.json create mode 100644 tests/fixtures/nws-weather-sta-valid.json diff --git a/CODEOWNERS b/CODEOWNERS index 71520e11acfecc..3ede39518c1874 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,6 +190,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nuki/* @pschmitt +homeassistant/components/nws/* @MatthewFlamm homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py new file mode 100644 index 00000000000000..dde2f6dee11c52 --- /dev/null +++ b/homeassistant/components/nws/__init__.py @@ -0,0 +1 @@ +"""NWS Integration.""" diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json new file mode 100644 index 00000000000000..b0e5fdb208844a --- /dev/null +++ b/homeassistant/components/nws/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "nws", + "name": "National Weather Service", + "documentation": "https://www.home-assistant.io/components/nws", + "dependencies": [], + "codeowners": ["@MatthewFlamm"], + "requirements": ["pynws==0.7.4"] +} diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py new file mode 100644 index 00000000000000..23cf84411a3788 --- /dev/null +++ b/homeassistant/components/nws/weather.py @@ -0,0 +1,378 @@ +"""Support for NWS weather service.""" +from collections import OrderedDict +from datetime import timedelta +from json import JSONDecodeError +import logging + +import aiohttp +from pynws import SimpleNWS +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, + PLATFORM_SCHEMA, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_PA, + PRESSURE_INHG, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.temperature import convert as convert_temperature + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data from National Weather Service/NOAA" + +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +CONF_STATION = "station" + +ATTR_FORECAST_DETAIL_DESCRIPTION = "detailed_description" +ATTR_FORECAST_PRECIP_PROB = "precipitation_probability" +ATTR_FORECAST_DAYTIME = "daytime" + +# Ordered so that a single condition can be chosen from multiple weather codes. +# Catalog of NWS icon weather codes listed at: +# https://api.weather.gov/icons +CONDITION_CLASSES = OrderedDict( + [ + ( + "exceptional", + [ + "Tornado", + "Hurricane conditions", + "Tropical storm conditions", + "Dust", + "Smoke", + "Haze", + "Hot", + "Cold", + ], + ), + ("snowy", ["Snow", "Sleet", "Blizzard"]), + ( + "snowy-rainy", + [ + "Rain/snow", + "Rain/sleet", + "Freezing rain/snow", + "Freezing rain", + "Rain/freezing rain", + ], + ), + ("hail", []), + ( + "lightning-rainy", + [ + "Thunderstorm (high cloud cover)", + "Thunderstorm (medium cloud cover)", + "Thunderstorm (low cloud cover)", + ], + ), + ("lightning", []), + ("pouring", []), + ( + "rainy", + [ + "Rain", + "Rain showers (high cloud cover)", + "Rain showers (low cloud cover)", + ], + ), + ("windy-variant", ["Mostly cloudy and windy", "Overcast and windy"]), + ( + "windy", + [ + "Fair/clear and windy", + "A few clouds and windy", + "Partly cloudy and windy", + ], + ), + ("fog", ["Fog/mist"]), + ("clear", ["Fair/clear"]), # sunny and clear-night + ("cloudy", ["Mostly cloudy", "Overcast"]), + ("partlycloudy", ["A few clouds", "Partly cloudy"]), + ] +) + +ERRORS = (aiohttp.ClientError, JSONDecodeError) + +FORECAST_MODE = ["daynight", "hourly"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Inclusive( + CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.longitude, + vol.Optional(CONF_MODE, default="daynight"): vol.In(FORECAST_MODE), + vol.Optional(CONF_STATION): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } +) + + +def convert_condition(time, weather): + """ + Convert NWS codes to HA condition. + + Choose first condition in CONDITION_CLASSES that exists in weather code. + If no match is found, return first condition from NWS + """ + conditions = [w[0] for w in weather] + prec_probs = [w[1] or 0 for w in weather] + + # Choose condition with highest priority. + cond = next( + ( + key + for key, value in CONDITION_CLASSES.items() + if any(condition in value for condition in conditions) + ), + conditions[0], + ) + + if cond == "clear": + if time == "day": + return "sunny", max(prec_probs) + if time == "night": + return "clear-night", max(prec_probs) + return cond, max(prec_probs) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the NWS weather platform.""" + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + station = config.get(CONF_STATION) + api_key = config[CONF_API_KEY] + mode = config[CONF_MODE] + + websession = async_get_clientsession(hass) + # ID request as being from HA, pynws prepends the api_key in addition + api_key_ha = f"{api_key} homeassistant" + nws = SimpleNWS(latitude, longitude, api_key_ha, mode, websession) + + _LOGGER.debug("Setting up station: %s", station) + try: + await nws.set_station(station) + except ERRORS as status: + _LOGGER.error( + "Error getting station list for %s: %s", (latitude, longitude), status + ) + raise PlatformNotReady + + _LOGGER.debug("Station list: %s", nws.stations) + _LOGGER.debug( + "Initialized for coordinates %s, %s -> station %s", + latitude, + longitude, + nws.station, + ) + + async_add_entities([NWSWeather(nws, mode, hass.config.units, config)], True) + + +class NWSWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, nws, mode, units, config): + """Initialise the platform with a data instance and station name.""" + self.nws = nws + self.station_name = config.get(CONF_NAME, self.nws.station) + self.is_metric = units.is_metric + self.mode = mode + + self.observation = None + self._forecast = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition.""" + _LOGGER.debug("Updating station observations %s", self.nws.station) + try: + await self.nws.update_observation() + except ERRORS as status: + _LOGGER.error( + "Error updating observation from station %s: %s", + self.nws.station, + status, + ) + else: + self.observation = self.nws.observation + _LOGGER.debug("Updating forecast") + try: + await self.nws.update_forecast() + except ERRORS as status: + _LOGGER.error( + "Error updating forecast from station %s: %s", self.nws.station, status + ) + return + self._forecast = self.nws.forecast + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self.station_name + + @property + def temperature(self): + """Return the current temperature.""" + temp_c = None + if self.observation: + temp_c = self.observation.get("temperature") + if temp_c: + return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) + return None + + @property + def pressure(self): + """Return the current pressure.""" + pressure_pa = None + if self.observation: + pressure_pa = self.observation.get("seaLevelPressure") + if pressure_pa is None: + return None + if self.is_metric: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_HPA) + pressure = round(pressure) + else: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_INHG) + pressure = round(pressure, 2) + return pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + humidity = None + if self.observation: + humidity = self.observation.get("relativeHumidity") + return humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + wind_m_s = None + if self.observation: + wind_m_s = self.observation.get("windSpeed") + if wind_m_s is None: + return None + wind_m_hr = wind_m_s * 3600 + + if self.is_metric: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_KILOMETERS) + else: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_MILES) + return round(wind) + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + wind_bearing = None + if self.observation: + wind_bearing = self.observation.get("windDirection") + return wind_bearing + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def condition(self): + """Return current condition.""" + weather = None + if self.observation: + weather = self.observation.get("iconWeather") + time = self.observation.get("iconTime") + + if weather: + cond, _ = convert_condition(time, weather) + return cond + return None + + @property + def visibility(self): + """Return visibility.""" + vis_m = None + if self.observation: + vis_m = self.observation.get("visibility") + if vis_m is None: + return None + + if self.is_metric: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_KILOMETERS) + else: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_MILES) + return round(vis, 0) + + @property + def forecast(self): + """Return forecast.""" + if self._forecast is None: + return None + forecast = [] + for forecast_entry in self._forecast: + data = { + ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get( + "detailedForecast" + ), + ATTR_FORECAST_TEMP: forecast_entry.get("temperature"), + ATTR_FORECAST_TIME: forecast_entry.get("startTime"), + } + + if self.mode == "daynight": + data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + time = forecast_entry.get("iconTime") + weather = forecast_entry.get("iconWeather") + if time and weather: + cond, precip = convert_condition(time, weather) + else: + cond, precip = None, None + data[ATTR_FORECAST_CONDITION] = cond + data[ATTR_FORECAST_PRECIP_PROB] = precip + + data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") + wind_speed = forecast_entry.get("windSpeedAvg") + if wind_speed: + if self.is_metric: + data[ATTR_FORECAST_WIND_SPEED] = round( + convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + ) + else: + data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed) + else: + data[ATTR_FORECAST_WIND_SPEED] = None + forecast.append(data) + return forecast diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8f276279ee5dfe..fd122f66ac2b2f 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -125,11 +125,11 @@ def precision(self): @property def state_attributes(self): """Return the state attributes.""" - data = { - ATTR_WEATHER_TEMPERATURE: show_temp( + data = {} + if self.temperature is not None: + data[ATTR_WEATHER_TEMPERATURE] = show_temp( self.hass, self.temperature, self.temperature_unit, self.precision ) - } humidity = self.humidity if humidity is not None: diff --git a/requirements_all.txt b/requirements_all.txt index d3d60e6a43e66e..c8e79616e17554 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1308,6 +1308,9 @@ pynuki==1.3.3 # homeassistant.components.nut pynut2==2.1.2 +# homeassistant.components.nws +pynws==0.7.4 + # homeassistant.components.nx584 pynx584==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5d139719ef6de..c90ad27554e263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -294,6 +294,9 @@ pymfy==0.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.nws +pynws==0.7.4 + # homeassistant.components.nx584 pynx584==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6643fcf7aa9a35..dd36771994bae2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -120,6 +120,7 @@ "pylitejet", "pymfy", "pymonoprice", + "pynws", "pynx584", "pyopenuv", "pyotp", diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py new file mode 100644 index 00000000000000..436d25750fc518 --- /dev/null +++ b/tests/components/nws/test_weather.py @@ -0,0 +1,274 @@ +"""Tests for the NWS weather component.""" +from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) + +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_INHG, + PRESSURE_PA, + PRESSURE_HPA, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.setup import async_setup_component + +from tests.common import load_fixture, assert_setup_component + +EXP_OBS_IMP = { + ATTR_WEATHER_TEMPERATURE: round( + convert_temperature(26.7, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ), + ATTR_WEATHER_WIND_BEARING: 190, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(2.6, LENGTH_METERS, LENGTH_MILES) * 3600 + ), + ATTR_WEATHER_PRESSURE: round( + convert_pressure(101040, PRESSURE_PA, PRESSURE_INHG), 2 + ), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(16090, LENGTH_METERS, LENGTH_MILES) + ), + ATTR_WEATHER_HUMIDITY: 64, +} + +EXP_OBS_METR = { + ATTR_WEATHER_TEMPERATURE: round(26.7), + ATTR_WEATHER_WIND_BEARING: 190, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(2.6, LENGTH_METERS, LENGTH_KILOMETERS) * 3600 + ), + ATTR_WEATHER_PRESSURE: round(convert_pressure(101040, PRESSURE_PA, PRESSURE_HPA)), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(16090, LENGTH_METERS, LENGTH_KILOMETERS) + ), + ATTR_WEATHER_HUMIDITY: 64, +} + +EXP_FORE_IMP = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: 70, + ATTR_FORECAST_WIND_SPEED: 10, + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + +EXP_FORE_METR = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: round(convert_temperature(70, TEMP_FAHRENHEIT, TEMP_CELSIUS)), + ATTR_FORECAST_WIND_SPEED: round( + convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) + ), + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + + +MINIMAL_CONFIG = { + "weather": { + "platform": "nws", + "api_key": "x@example.com", + "latitude": 40.0, + "longitude": -85.0, + } +} + +INVALID_CONFIG = { + "weather": {"platform": "nws", "api_key": "x@example.com", "latitude": 40.0} +} + +STAURL = "https://api.weather.gov/points/{},{}/stations" +OBSURL = "https://api.weather.gov/stations/{}/observations/" +FORCURL = "https://api.weather.gov/points/{},{}/forecast" + + +async def test_imperial(hass, aioclient_mock): + """Test with imperial units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "sunny" + + data = state.attributes + for key, value in EXP_OBS_IMP.items(): + assert data.get(key) == value + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key, value in EXP_FORE_IMP.items(): + assert forecast[0].get(key) == value + + +async def test_metric(hass, aioclient_mock): + """Test with metric units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = METRIC_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "sunny" + + data = state.attributes + for key, value in EXP_OBS_METR.items(): + assert data.get(key) == value + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key, value in EXP_FORE_METR.items(): + assert forecast[0].get(key) == value + + +async def test_none(hass, aioclient_mock): + """Test with imperial units.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-null.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + assert state.state == "unknown" + + data = state.attributes + for key in EXP_OBS_IMP: + assert data.get(key) is None + assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) + for key in EXP_FORE_IMP: + assert forecast[0].get(key) is None + + +async def test_fail_obs(hass, aioclient_mock): + """Test failing observation/forecast update.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + status=400, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), + text=load_fixture("nws-weather-fore-valid.json"), + status=400, + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state + + +async def test_fail_stn(hass, aioclient_mock): + """Test failing station update.""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), + text=load_fixture("nws-weather-sta-valid.json"), + status=400, + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(1, "weather"): + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + + state = hass.states.get("weather.kmie") + assert state is None + + +async def test_invalid_config(hass, aioclient_mock): + """Test invalid config..""" + aioclient_mock.get( + STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") + ) + aioclient_mock.get( + OBSURL.format("KMIE"), + text=load_fixture("nws-weather-obs-valid.json"), + params={"limit": 1}, + ) + aioclient_mock.get( + FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") + ) + + hass.config.units = IMPERIAL_SYSTEM + + with assert_setup_component(0, "weather"): + await async_setup_component(hass, "weather", INVALID_CONFIG) + + state = hass.states.get("weather.kmie") + assert state is None diff --git a/tests/fixtures/nws-weather-fore-null.json b/tests/fixtures/nws-weather-fore-null.json new file mode 100644 index 00000000000000..6085bcdada9b01 --- /dev/null +++ b/tests/fixtures/nws-weather-fore-null.json @@ -0,0 +1,80 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#" + } + ], + "type": "Feature", + "geometry": { + "type": "GeometryCollection", + "geometries": [ + { + "type": "Point", + "coordinates": [ + -85.014692800000006, + 39.993574700000003 + ] + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -85.027968599999994, + 40.005368300000001 + ], + [ + -85.0300814, + 39.983399599999998 + ], + [ + -85.001420100000004, + 39.981779299999999 + ], + [ + -84.999301200000005, + 40.0037479 + ], + [ + -85.027968599999994, + 40.005368300000001 + ] + ] + ] + } + ] + }, + "properties": { + "updated": "2019-08-12T23:17:40+00:00", + "units": "us", + "forecastGenerator": "BaselineForecastGenerator", + "generatedAt": "2019-08-13T00:33:19+00:00", + "updateTime": "2019-08-12T23:17:40+00:00", + "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", + "elevation": { + "value": 366.06479999999999, + "unitCode": "unit:m" + }, + "periods": [ + { + "number": null, + "name": null, + "startTime": null, + "endTime": null, + "isDaytime": null, + "temperature": null, + "temperatureUnit": null, + "temperatureTrend": null, + "windSpeed": null, + "windDirection": null, + "icon": null, + "shortForecast": null, + "detailedForecast": null + } + ] + } +} diff --git a/tests/fixtures/nws-weather-fore-valid.json b/tests/fixtures/nws-weather-fore-valid.json new file mode 100644 index 00000000000000..b3f4f4ccea8c6c --- /dev/null +++ b/tests/fixtures/nws-weather-fore-valid.json @@ -0,0 +1,80 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#" + } + ], + "type": "Feature", + "geometry": { + "type": "GeometryCollection", + "geometries": [ + { + "type": "Point", + "coordinates": [ + -85.014692800000006, + 39.993574700000003 + ] + }, + { + "type": "Polygon", + "coordinates": [ + [ + [ + -85.027968599999994, + 40.005368300000001 + ], + [ + -85.0300814, + 39.983399599999998 + ], + [ + -85.001420100000004, + 39.981779299999999 + ], + [ + -84.999301200000005, + 40.0037479 + ], + [ + -85.027968599999994, + 40.005368300000001 + ] + ] + ] + } + ] + }, + "properties": { + "updated": "2019-08-12T23:17:40+00:00", + "units": "us", + "forecastGenerator": "BaselineForecastGenerator", + "generatedAt": "2019-08-13T00:33:19+00:00", + "updateTime": "2019-08-12T23:17:40+00:00", + "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", + "elevation": { + "value": 366.06479999999999, + "unitCode": "unit:m" + }, + "periods": [ + { + "number": 1, + "name": "Tonight", + "startTime": "2019-08-12T20:00:00-04:00", + "endTime": "2019-08-13T06:00:00-04:00", + "isDaytime": false, + "temperature": 70, + "temperatureUnit": "F", + "temperatureTrend": null, + "windSpeed": "7 to 13 mph", + "windDirection": "S", + "icon": "https://api.weather.gov/icons/land/night/tsra,40/tsra,90?size=medium", + "shortForecast": "Showers And Thunderstorms", + "detailedForecast": "A detailed forecast." + } + ] + } +} diff --git a/tests/fixtures/nws-weather-obs-null.json b/tests/fixtures/nws-weather-obs-null.json new file mode 100644 index 00000000000000..36ae66283e502f --- /dev/null +++ b/tests/fixtures/nws-weather-obs-null.json @@ -0,0 +1,161 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.400000000000006, + 40.25 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "@type": "wx:ObservationStation", + "elevation": { + "value": 286, + "unitCode": "unit:m" + }, + "station": "https://api.weather.gov/stations/KMIE", + "timestamp": "2019-08-12T23:53:00+00:00", + "rawMessage": null, + "textDescription": "Clear", + "icon": null, + "presentWeather": [], + "temperature": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "dewpoint": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "windDirection": { + "value": null, + "unitCode": "unit:degree_(angle)", + "qualityControl": "qc:V" + }, + "windSpeed": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:V" + }, + "windGust": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:Z" + }, + "barometricPressure": { + "value": null, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "seaLevelPressure": { + "value": null, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "visibility": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "maxTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "minTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "precipitationLastHour": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast3Hours": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast6Hours": { + "value": 0, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "relativeHumidity": { + "value": null, + "unitCode": "unit:percent", + "qualityControl": "qc:C" + }, + "windChill": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "heatIndex": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "cloudLayers": [ + { + "base": { + "value": null, + "unitCode": "unit:m" + }, + "amount": "CLR" + } + ] + } + } + ] +} diff --git a/tests/fixtures/nws-weather-obs-valid.json b/tests/fixtures/nws-weather-obs-valid.json new file mode 100644 index 00000000000000..a6d307fc9b13f2 --- /dev/null +++ b/tests/fixtures/nws-weather-obs-valid.json @@ -0,0 +1,161 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.400000000000006, + 40.25 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", + "@type": "wx:ObservationStation", + "elevation": { + "value": 286, + "unitCode": "unit:m" + }, + "station": "https://api.weather.gov/stations/KMIE", + "timestamp": "2019-08-12T23:53:00+00:00", + "rawMessage": "KMIE 122353Z 19005KT 10SM CLR 27/19 A2987 RMK AO2 SLP104 60000 T02670194 10272 20250 58002", + "textDescription": "Clear", + "icon": "https://api.weather.gov/icons/land/day/skc?size=medium", + "presentWeather": [], + "temperature": { + "value": 26.700000000000045, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "dewpoint": { + "value": 19.400000000000034, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "windDirection": { + "value": 190, + "unitCode": "unit:degree_(angle)", + "qualityControl": "qc:V" + }, + "windSpeed": { + "value": 2.6000000000000001, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:V" + }, + "windGust": { + "value": null, + "unitCode": "unit:m_s-1", + "qualityControl": "qc:Z" + }, + "barometricPressure": { + "value": 101150, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "seaLevelPressure": { + "value": 101040, + "unitCode": "unit:Pa", + "qualityControl": "qc:V" + }, + "visibility": { + "value": 16090, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "maxTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "minTemperatureLast24Hours": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": null + }, + "precipitationLastHour": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast3Hours": { + "value": null, + "unitCode": "unit:m", + "qualityControl": "qc:Z" + }, + "precipitationLast6Hours": { + "value": 0, + "unitCode": "unit:m", + "qualityControl": "qc:C" + }, + "relativeHumidity": { + "value": 64.292485914891955, + "unitCode": "unit:percent", + "qualityControl": "qc:C" + }, + "windChill": { + "value": null, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "heatIndex": { + "value": 27.981288713580284, + "unitCode": "unit:degC", + "qualityControl": "qc:V" + }, + "cloudLayers": [ + { + "base": { + "value": null, + "unitCode": "unit:m" + }, + "amount": "CLR" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/nws-weather-sta-valid.json b/tests/fixtures/nws-weather-sta-valid.json new file mode 100644 index 00000000000000..b4fe086366c8fb --- /dev/null +++ b/tests/fixtures/nws-weather-sta-valid.json @@ -0,0 +1,996 @@ +{ + "@context": [ + "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", + { + "wx": "https://api.weather.gov/ontology#", + "s": "https://schema.org/", + "geo": "http://www.opengis.net/ont/geosparql#", + "unit": "http://codes.wmo.int/common/unit/", + "@vocab": "https://api.weather.gov/ontology#", + "geometry": { + "@id": "s:GeoCoordinates", + "@type": "geo:wktLiteral" + }, + "city": "s:addressLocality", + "state": "s:addressRegion", + "distance": { + "@id": "s:Distance", + "@type": "s:QuantitativeValue" + }, + "bearing": { + "@type": "s:QuantitativeValue" + }, + "value": { + "@id": "s:value" + }, + "unitCode": { + "@id": "s:unitCode", + "@type": "@id" + }, + "forecastOffice": { + "@type": "@id" + }, + "forecastGridData": { + "@type": "@id" + }, + "publicZone": { + "@type": "@id" + }, + "county": { + "@type": "@id" + }, + "observationStations": { + "@container": "@list", + "@type": "@id" + } + } + ], + "type": "FeatureCollection", + "features": [ + { + "id": "https://api.weather.gov/stations/KMIE", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.393609999999995, + 40.234169999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMIE", + "@type": "wx:ObservationStation", + "elevation": { + "value": 284.988, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMIE", + "name": "Muncie, Delaware County-Johnson Field", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KVES", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.531899899999999, + 40.2044 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KVES", + "@type": "wx:ObservationStation", + "elevation": { + "value": 306.93360000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KVES", + "name": "Versailles Darke County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KAID", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.609769999999997, + 40.106119999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KAID", + "@type": "wx:ObservationStation", + "elevation": { + "value": 276.14879999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KAID", + "name": "Anderson Municipal Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KDAY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.218609999999998, + 39.906109999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KDAY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 306.93360000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KDAY", + "name": "Dayton, Cox Dayton International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KGEZ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.799819999999997, + 39.585459999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KGEZ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 244.1448, + "unitCode": "unit:m" + }, + "stationIdentifier": "KGEZ", + "name": "Shelbyville Municipal Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KMGY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.224720000000005, + 39.588889999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMGY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 291.9984, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMGY", + "name": "Dayton, Dayton-Wright Brothers Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KHAO", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.520610000000005, + 39.36121 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KHAO", + "@type": "wx:ObservationStation", + "elevation": { + "value": 185.0136, + "unitCode": "unit:m" + }, + "stationIdentifier": "KHAO", + "name": "Butler County Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFFO", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.049999999999997, + 39.833329900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFFO", + "@type": "wx:ObservationStation", + "elevation": { + "value": 250.85040000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFFO", + "name": "Dayton / Wright-Patterson Air Force Base", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCVG", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.672290000000004, + 39.044559999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCVG", + "@type": "wx:ObservationStation", + "elevation": { + "value": 262.12799999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCVG", + "name": "Cincinnati/Northern Kentucky International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KEDJ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.819199999999995, + 40.372300000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KEDJ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 341.98560000000003, + "unitCode": "unit:m" + }, + "stationIdentifier": "KEDJ", + "name": "Bellefontaine Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFWA", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.206370000000007, + 40.97251 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFWA", + "@type": "wx:ObservationStation", + "elevation": { + "value": 242.9256, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFWA", + "name": "Fort Wayne International Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KBAK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.900000000000006, + 39.266669999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KBAK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 199.94880000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KBAK", + "name": "Columbus / Bakalar", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KEYE", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -86.295829999999995, + 39.825000000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KEYE", + "@type": "wx:ObservationStation", + "elevation": { + "value": 249.93600000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KEYE", + "name": "Indianapolis, Eagle Creek Airpark", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KLUK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.41583, + 39.105829999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLUK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 146.9136, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLUK", + "name": "Cincinnati, Cincinnati Municipal Airport Lunken Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KIND", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -86.281599999999997, + 39.725180000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KIND", + "@type": "wx:ObservationStation", + "elevation": { + "value": 240.792, + "unitCode": "unit:m" + }, + "stationIdentifier": "KIND", + "name": "Indianapolis International Airport", + "timeZone": "America/Indiana/Indianapolis" + } + }, + { + "id": "https://api.weather.gov/stations/KAOH", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.021389999999997, + 40.708060000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KAOH", + "@type": "wx:ObservationStation", + "elevation": { + "value": 296.87520000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KAOH", + "name": "Lima, Lima Allen County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KI69", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.2102, + 39.078400000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KI69", + "@type": "wx:ObservationStation", + "elevation": { + "value": 256.94640000000004, + "unitCode": "unit:m" + }, + "stationIdentifier": "KI69", + "name": "Batavia Clermont County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KILN", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.779169899999999, + 39.428330000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KILN", + "@type": "wx:ObservationStation", + "elevation": { + "value": 327.96480000000003, + "unitCode": "unit:m" + }, + "stationIdentifier": "KILN", + "name": "Wilmington, Airborne Airpark Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMRT", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.351600000000005, + 40.224699999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMRT", + "@type": "wx:ObservationStation", + "elevation": { + "value": 311.20080000000002, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMRT", + "name": "Marysville Union County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KTZR", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.137219999999999, + 39.900829999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KTZR", + "@type": "wx:ObservationStation", + "elevation": { + "value": 276.14879999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KTZR", + "name": "Columbus, Bolton Field Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFDY", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.668610000000001, + 41.01361 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFDY", + "@type": "wx:ObservationStation", + "elevation": { + "value": 248.10720000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFDY", + "name": "Findlay, Findlay Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KDLZ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.114800000000002, + 40.279699999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KDLZ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 288.036, + "unitCode": "unit:m" + }, + "stationIdentifier": "KDLZ", + "name": "Delaware Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KOSU", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.0780599, + 40.078060000000001 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KOSU", + "@type": "wx:ObservationStation", + "elevation": { + "value": 274.92959999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KOSU", + "name": "Columbus, Ohio State University Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLCK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.933329999999998, + 39.816670000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLCK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 227.07600000000002, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLCK", + "name": "Rickenbacker Air National Guard Base", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMNN", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.068330000000003, + 40.616669999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMNN", + "@type": "wx:ObservationStation", + "elevation": { + "value": 302.97120000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMNN", + "name": "Marion, Marion Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCMH", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.876390000000001, + 39.994999999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCMH", + "@type": "wx:ObservationStation", + "elevation": { + "value": 248.10720000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCMH", + "name": "Columbus - John Glenn Columbus International Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFGX", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.743399999999994, + 38.541800000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFGX", + "@type": "wx:ObservationStation", + "elevation": { + "value": 277.9776, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFGX", + "name": "Flemingsburg Fleming-Mason Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KFFT", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.903329999999997, + 38.184719999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KFFT", + "@type": "wx:ObservationStation", + "elevation": { + "value": 245.0592, + "unitCode": "unit:m" + }, + "stationIdentifier": "KFFT", + "name": "Frankfort, Capital City Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLHQ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.663330000000002, + 39.757219900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLHQ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 263.95679999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLHQ", + "name": "Lancaster, Fairfield County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLOU", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.663610000000006, + 38.227780000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLOU", + "@type": "wx:ObservationStation", + "elevation": { + "value": 166.11600000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLOU", + "name": "Louisville, Bowman Field Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KSDF", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -85.72972, + 38.177219999999998 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KSDF", + "@type": "wx:ObservationStation", + "elevation": { + "value": 150.876, + "unitCode": "unit:m" + }, + "stationIdentifier": "KSDF", + "name": "Louisville, Standiford Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KVTA", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.462500000000006, + 40.022779999999997 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KVTA", + "@type": "wx:ObservationStation", + "elevation": { + "value": 269.13839999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KVTA", + "name": "Newark, Newark Heath Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KLEX", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -84.6114599, + 38.033900000000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KLEX", + "@type": "wx:ObservationStation", + "elevation": { + "value": 291.084, + "unitCode": "unit:m" + }, + "stationIdentifier": "KLEX", + "name": "Lexington Blue Grass Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KMFD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.517780000000002, + 40.820279900000003 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KMFD", + "@type": "wx:ObservationStation", + "elevation": { + "value": 395.02080000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KMFD", + "name": "Mansfield - Mansfield Lahm Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KZZV", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.892219999999995, + 39.94444 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KZZV", + "@type": "wx:ObservationStation", + "elevation": { + "value": 274.01519999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KZZV", + "name": "Zanesville, Zanesville Municipal Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KHTS", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.555000000000007, + 38.365000000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KHTS", + "@type": "wx:ObservationStation", + "elevation": { + "value": 252.06960000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KHTS", + "name": "Huntington, Tri-State Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KBJJ", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.886669999999995, + 40.873060000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KBJJ", + "@type": "wx:ObservationStation", + "elevation": { + "value": 345.94800000000004, + "unitCode": "unit:m" + }, + "stationIdentifier": "KBJJ", + "name": "Wooster, Wayne County Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KPHD", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.423609999999996, + 40.471939900000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KPHD", + "@type": "wx:ObservationStation", + "elevation": { + "value": 271.88159999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KPHD", + "name": "New Philadelphia, Harry Clever Field", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KPKB", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.439170000000004, + 39.344999999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KPKB", + "@type": "wx:ObservationStation", + "elevation": { + "value": 262.12799999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KPKB", + "name": "Parkersburg, Mid-Ohio Valley Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCAK", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.443430000000006, + 40.918109999999999 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCAK", + "@type": "wx:ObservationStation", + "elevation": { + "value": 369.11279999999999, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCAK", + "name": "Akron Canton Regional Airport", + "timeZone": "America/New_York" + } + }, + { + "id": "https://api.weather.gov/stations/KCRW", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -81.591390000000004, + 38.379440000000002 + ] + }, + "properties": { + "@id": "https://api.weather.gov/stations/KCRW", + "@type": "wx:ObservationStation", + "elevation": { + "value": 299.00880000000001, + "unitCode": "unit:m" + }, + "stationIdentifier": "KCRW", + "name": "Charleston, Yeager Airport", + "timeZone": "America/New_York" + } + } + ], + "observationStations": [ + "https://api.weather.gov/stations/KMIE", + "https://api.weather.gov/stations/KVES", + "https://api.weather.gov/stations/KAID", + "https://api.weather.gov/stations/KDAY", + "https://api.weather.gov/stations/KGEZ", + "https://api.weather.gov/stations/KMGY", + "https://api.weather.gov/stations/KHAO", + "https://api.weather.gov/stations/KFFO", + "https://api.weather.gov/stations/KCVG", + "https://api.weather.gov/stations/KEDJ", + "https://api.weather.gov/stations/KFWA", + "https://api.weather.gov/stations/KBAK", + "https://api.weather.gov/stations/KEYE", + "https://api.weather.gov/stations/KLUK", + "https://api.weather.gov/stations/KIND", + "https://api.weather.gov/stations/KAOH", + "https://api.weather.gov/stations/KI69", + "https://api.weather.gov/stations/KILN", + "https://api.weather.gov/stations/KMRT", + "https://api.weather.gov/stations/KTZR", + "https://api.weather.gov/stations/KFDY", + "https://api.weather.gov/stations/KDLZ", + "https://api.weather.gov/stations/KOSU", + "https://api.weather.gov/stations/KLCK", + "https://api.weather.gov/stations/KMNN", + "https://api.weather.gov/stations/KCMH", + "https://api.weather.gov/stations/KFGX", + "https://api.weather.gov/stations/KFFT", + "https://api.weather.gov/stations/KLHQ", + "https://api.weather.gov/stations/KLOU", + "https://api.weather.gov/stations/KSDF", + "https://api.weather.gov/stations/KVTA", + "https://api.weather.gov/stations/KLEX", + "https://api.weather.gov/stations/KMFD", + "https://api.weather.gov/stations/KZZV", + "https://api.weather.gov/stations/KHTS", + "https://api.weather.gov/stations/KBJJ", + "https://api.weather.gov/stations/KPHD", + "https://api.weather.gov/stations/KPKB", + "https://api.weather.gov/stations/KCAK", + "https://api.weather.gov/stations/KCRW" + ] +} \ No newline at end of file From 55031e6ea497b3219d90dc3a602b97172651c977 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 23 Aug 2019 06:58:24 -0700 Subject: [PATCH 175/213] Bump androidtv to 0.0.24 (#26158) * Bump androidtv to 0.0.24 * Add unique ID for Fire TV (not just Android TV) --- homeassistant/components/androidtv/manifest.json | 2 +- .../components/androidtv/media_player.py | 15 ++++++++------- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 24eb61d52b00e8..047eaaaf5db9c1 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.23" + "androidtv==0.0.24" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index ef9293381fd491..db4ff9e851ec82 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -270,6 +270,9 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): self._apps.update(apps) self._keys = KEYS + self._device_properties = self.aftv.device_properties + self._unique_id = self._device_properties.get("serialno") + self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command @@ -338,6 +341,11 @@ def state(self): """Return the state of the player.""" return self._state + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + @adb_decorator() def media_play(self): """Send play command.""" @@ -412,9 +420,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._device = None - self._device_properties = self.aftv.device_properties self._is_volume_muted = None - self._unique_id = self._device_properties.get("serialno") self._volume_level = None @adb_decorator(override_available=True) @@ -454,11 +460,6 @@ def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_ANDROIDTV - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - @property def volume_level(self): """Return the volume level.""" diff --git a/requirements_all.txt b/requirements_all.txt index c8e79616e17554..f10b74b9594bdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.23 +androidtv==0.0.24 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 From 1efa29d6ff60409435fbdfb906ee737c2123de6d Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 23 Aug 2019 16:59:25 +0300 Subject: [PATCH 176/213] CoolMaster: Change auto to heat_cool (#26144) --- homeassistant/components/coolmaster/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 7379d66777b01c..8a319c655f6f0d 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -7,7 +7,7 @@ from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -33,14 +33,14 @@ HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_FAN_ONLY, ] CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, - "auto": HVAC_MODE_AUTO, + "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, "fan": HVAC_MODE_FAN_ONLY, } From decf13b94819a8e7a9135393e87f3435128bd66e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Aug 2019 18:53:33 +0200 Subject: [PATCH 177/213] Use literal string interpolation in core (f-strings) (#26166) --- homeassistant/__main__.py | 6 ++--- homeassistant/auth/__init__.py | 10 +++---- homeassistant/auth/auth_store.py | 2 +- homeassistant/auth/mfa_modules/__init__.py | 6 ++--- homeassistant/auth/providers/__init__.py | 10 +++---- homeassistant/bootstrap.py | 2 +- homeassistant/components/api/__init__.py | 6 ++--- .../components/automation/__init__.py | 6 ++--- homeassistant/components/config/__init__.py | 12 ++++----- .../components/configurator/__init__.py | 4 +-- homeassistant/components/demo/camera.py | 2 +- homeassistant/components/demo/media_player.py | 2 +- homeassistant/components/demo/vacuum.py | 2 +- homeassistant/components/frontend/__init__.py | 8 ++---- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/hassio/handler.py | 6 ++--- homeassistant/components/hassio/http.py | 2 +- homeassistant/components/hassio/ingress.py | 10 +++---- homeassistant/components/http/__init__.py | 14 ++++------ homeassistant/components/http/ban.py | 2 +- .../components/http/data_validator.py | 4 +-- homeassistant/components/http/static.py | 2 +- .../components/input_number/__init__.py | 6 ++--- .../components/input_text/__init__.py | 4 +-- .../components/integration/sensor.py | 2 +- .../components/intent_script/__init__.py | 2 +- .../components/python_script/__init__.py | 8 +++--- .../components/recorder/migration.py | 8 +++--- homeassistant/components/script/__init__.py | 2 +- .../components/system_log/__init__.py | 2 +- homeassistant/components/tts/__init__.py | 26 ++++++++----------- .../components/utility_meter/__init__.py | 2 +- .../components/utility_meter/sensor.py | 2 +- homeassistant/components/zeroconf/__init__.py | 2 +- homeassistant/config.py | 10 +++---- homeassistant/core.py | 2 +- homeassistant/data_entry_flow.py | 2 +- homeassistant/exceptions.py | 6 ++--- homeassistant/helpers/check_config.py | 14 +++++----- homeassistant/helpers/entity.py | 8 +++--- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/entity_platform.py | 8 +++--- homeassistant/helpers/entity_registry.py | 4 +-- homeassistant/helpers/intent.py | 14 ++++------ homeassistant/helpers/temperature.py | 2 +- homeassistant/helpers/template.py | 12 ++++----- homeassistant/helpers/translation.py | 6 ++--- homeassistant/loader.py | 23 +++++++--------- homeassistant/requirements.py | 4 +-- homeassistant/scripts/benchmark/__init__.py | 2 +- homeassistant/scripts/credstash.py | 12 ++++----- homeassistant/scripts/keyring.py | 18 ++++++------- homeassistant/setup.py | 4 +-- homeassistant/util/__init__.py | 2 +- homeassistant/util/distance.py | 2 +- homeassistant/util/dt.py | 4 +-- homeassistant/util/pressure.py | 2 +- homeassistant/util/volume.py | 2 +- homeassistant/util/yaml/loader.py | 4 +-- script/gen_requirements_all.py | 22 ++++++---------- script/hassfest/__main__.py | 2 +- script/hassfest/dependencies.py | 2 +- script/hassfest/model.py | 8 +++--- script/lazytox.py | 4 +-- script/translations_download_split.py | 15 +++-------- script/translations_upload_merge.py | 2 +- script/version_bump.py | 16 ++++-------- 67 files changed, 180 insertions(+), 246 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 8ec2a8c2d3cb98..8765ee6c82260f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -168,7 +168,7 @@ def get_arguments() -> argparse.Namespace: parser.add_argument( "--runner", action="store_true", - help="On restart exit with code {}".format(RESTART_EXIT_CODE), + help=f"On restart exit with code {RESTART_EXIT_CODE}", ) parser.add_argument( "--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts" @@ -240,7 +240,7 @@ def write_pid(pid_file: str) -> None: with open(pid_file, "w") as file: file.write(str(pid)) except IOError: - print("Fatal Error: Unable to write pid file {}".format(pid_file)) + print(f"Fatal Error: Unable to write pid file {pid_file}") sys.exit(1) @@ -326,7 +326,7 @@ def try_to_restart() -> None: thread.is_alive() and not thread.daemon for thread in threading.enumerate() ) if nthreads > 1: - sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads)) + sys.stderr.write(f"Found {nthreads} non-daemonic threads.\n") # Somehow we sometimes seem to trigger an assertion in the python threading # module. It seems we find threads that have no associated OS level thread diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 2641f0b8f7ee4f..e2778e9f45b58f 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -278,9 +278,7 @@ async def async_enable_user_mfa( module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError( - "Unable find multi-factor auth module: {}".format(mfa_module_id) - ) + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") await module.async_setup_user(user.id, data) @@ -295,9 +293,7 @@ async def async_disable_user_mfa( module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError( - "Unable find multi-factor auth module: {}".format(mfa_module_id) - ) + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") await module.async_depose_user(user.id) @@ -356,7 +352,7 @@ async def async_create_refresh_token( ): # Each client_name can only have one # long_lived_access_token type of refresh token - raise ValueError("{} already exists".format(client_name)) + raise ValueError(f"{client_name} already exists") return await self._store.async_create_refresh_token( user, diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 82db0bcf7a90f1..894819fb3c7bf6 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -94,7 +94,7 @@ async def async_create_user( for group_id in group_ids or []: group = self._groups.get(group_id) if group is None: - raise ValueError("Invalid group specified {}".format(group_id)) + raise ValueError(f"Invalid group specified {group_id}") groups.append(group) kwargs = { diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 5481b8fe08bfae..baccedeabbff23 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -144,15 +144,13 @@ async def auth_mfa_module_from_config( async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType: """Load an mfa auth module.""" - module_path = "homeassistant.auth.mfa_modules.{}".format(module_name) + module_path = f"homeassistant.auth.mfa_modules.{module_name}" try: module = importlib.import_module(module_path) except ImportError as err: _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) - raise HomeAssistantError( - "Unable to load mfa module {}: {}".format(module_name, err) - ) + raise HomeAssistantError(f"Unable to load mfa module {module_name}: {err}") if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index c35af2e0b96ac6..ee9ef8f94cd0ca 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -144,14 +144,10 @@ async def load_auth_provider_module( ) -> types.ModuleType: """Load an auth provider.""" try: - module = importlib.import_module( - "homeassistant.auth.providers.{}".format(provider) - ) + module = importlib.import_module(f"homeassistant.auth.providers.{provider}") except ImportError as err: _LOGGER.error("Unable to load auth provider %s: %s", provider, err) - raise HomeAssistantError( - "Unable to load auth provider {}: {}".format(provider, err) - ) + raise HomeAssistantError(f"Unable to load auth provider {provider}: {err}") if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module @@ -166,7 +162,7 @@ async def load_auth_provider_module( # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore await requirements.async_process_requirements( - hass, "auth provider {}".format(provider), reqs + hass, f"auth provider {provider}", reqs ) processed.add(provider) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b0eab0da0f32c7..3e71a588af0c09 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -163,7 +163,7 @@ def async_enable_logging( # ensure that the handlers it sets up wraps the correct streams. logging.basicConfig(level=logging.INFO) - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + colorfmt = f"%(log_color)s{fmt}%(reset)s" logging.getLogger().handlers[0].setFormatter( ColoredFormatter( colorfmt, diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index ee991535104716..d4faa55ed8c058 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -138,7 +138,7 @@ async def forward_events(event): if payload is stop_obj: break - msg = "data: {}\n\n".format(payload) + msg = f"data: {payload}\n\n" _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip()) await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: @@ -316,7 +316,7 @@ async def post(self, request, event_type): event_type, event_data, ha.EventOrigin.remote, self.context(request) ) - return self.json_message("Event {} fired.".format(event_type)) + return self.json_message(f"Event {event_type} fired.") class APIServicesView(HomeAssistantView): @@ -388,7 +388,7 @@ async def post(self, request): return tpl.async_render(data.get("variables")) except (ValueError, TemplateError) as ex: return self.json_message( - "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST + f"Error rendering template: {ex}", HTTP_BAD_REQUEST ) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5de9336d1d97e3..1cffd361b19210 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -143,7 +143,7 @@ async def trigger_service_handler(service_call): async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] - method = "async_{}".format(service_call.service) + method = f"async_{service_call.service}" for entity in await component.async_extract_from_service(service_call): tasks.append(getattr(entity, method)()) @@ -378,7 +378,7 @@ async def _async_process_config(hass, config, component): for list_no, config_block in enumerate(conf): automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) + name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" hidden = config_block[CONF_HIDE_ENTITY] initial_state = config_block.get(CONF_INITIAL_STATE) @@ -431,7 +431,7 @@ async def action(entity_id, variables, context): await script_obj.async_run(variables, context) except Exception as err: # pylint: disable=broad-except script_obj.async_log_exception( - _LOGGER, "Error while executing automation {}".format(entity_id), err + _LOGGER, f"Error while executing automation {entity_id}", err ) return action diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5de11a032c5f73..6d4b465fceb3dc 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -36,7 +36,7 @@ async def async_setup(hass, config): async def setup_panel(panel_name): """Set up a panel.""" - panel = importlib.import_module(".{}".format(panel_name), __name__) + panel = importlib.import_module(f".{panel_name}", __name__) if not panel: return @@ -44,7 +44,7 @@ async def setup_panel(panel_name): success = await panel.async_setup(hass) if success: - key = "{}.{}".format(DOMAIN, panel_name) + key = f"{DOMAIN}.{panel_name}" hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) @callback @@ -82,8 +82,8 @@ def __init__( post_write_hook=None, ): """Initialize a config view.""" - self.url = "/api/config/%s/%s/{config_key}" % (component, config_type) - self.name = "api:config:%s:%s" % (component, config_type) + self.url = f"/api/config/{component}/{config_type}/{{config_key}}" + self.name = f"api:config:{component}:{config_type}" self.path = path self.key_schema = key_schema self.data_schema = data_schema @@ -126,14 +126,14 @@ async def post(self, request, config_key): try: self.key_schema(config_key) except vol.Invalid as err: - return self.json_message("Key malformed: {}".format(err), 400) + return self.json_message(f"Key malformed: {err}", 400) try: # We just validate, we don't store that data because # we don't want to store the defaults. self.data_schema(data) except vol.Invalid as err: - return self.json_message("Message malformed: {}".format(err), 400) + return self.json_message(f"Message malformed: {err}", 400) hass = request.app["hass"] path = hass.config.path(self.path) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 99995959c23794..f3b2a41e9177a4 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -61,10 +61,10 @@ def async_request_config( Will return an ID to be used for sequent calls. """ if link_name is not None and link_url is not None: - description += "\n\n[{}]({})".format(link_name, link_url) + description += f"\n\n[{link_name}]({link_url})" if description_image is not None: - description += "\n\n![Description image]({})".format(description_image) + description += f"\n\n![Description image]({description_image})" instance = hass.data.get(_KEY_INSTANCE) diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 7ac5fc17c69993..0cd77b6112eca8 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -28,7 +28,7 @@ def camera_image(self): self._images_index = (self._images_index + 1) % 4 image_path = os.path.join( - os.path.dirname(__file__), "demo_{}.jpg".format(self._images_index) + os.path.dirname(__file__), f"demo_{self._images_index}.jpg" ) _LOGGER.debug("Loading camera_image: %s", image_path) with open(image_path, "rb") as file: diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index e3f69be3020f3e..fb64f8015c0f4b 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -417,7 +417,7 @@ def media_image_url(self): @property def media_title(self): """Return the title of current playing media.""" - return "Chapter {}".format(self._cur_episode) + return f"Chapter {self._cur_episode}" @property def media_series_title(self): diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index ffd3e768b116ea..2ba704d39252bc 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -244,7 +244,7 @@ def send_command(self, command, params=None, **kwargs): if self.supported_features & SUPPORT_SEND_COMMAND == 0: return - self._status = "Executing {}({})".format(command, params) + self._status = f"Executing {command}({params})" self._state = True self.schedule_update_ha_state() diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d8790b746bec85..7298ce8c1d086a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -274,9 +274,7 @@ async def async_setup(hass, config): ("frontend_latest", True), ("frontend_es5", True), ): - hass.http.register_static_path( - "/{}".format(path), str(root_path / path), should_cache - ) + hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache) hass.http.register_static_path( "/auth/authorize", str(root_path / "authorize.html"), False @@ -294,9 +292,7 @@ async def async_setup(hass, config): # To smooth transition to new urls, add redirects to new urls of dev tools # Added June 27, 2019. Can be removed in 2021. for panel in ("event", "info", "service", "state", "template", "mqtt"): - hass.http.register_redirect( - "/dev-{}".format(panel), "/developer-tools/{}".format(panel) - ) + hass.http.register_redirect(f"/dev-{panel}", f"/developer-tools/{panel}") async_register_built_in_panel( hass, diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 801c20b5c2bf0d..6603728e0370fb 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -271,7 +271,7 @@ async def async_handle_core_service(call): hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", "Config validating", - "{0}.check_config".format(HASS_DOMAIN), + f"{HASS_DOMAIN}.check_config", ) return diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 10f21556fb3ca3..5213443614cbe7 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -80,7 +80,7 @@ def get_addon_info(self, addon): This method return a coroutine. """ - return self.send_command("/addons/{}/info".format(addon), method="get") + return self.send_command(f"/addons/{addon}/info", method="get") @_api_data def get_ingress_panels(self): @@ -120,7 +120,7 @@ def get_discovery_message(self, uuid): This method return a coroutine. """ - return self.send_command("/discovery/{}".format(uuid), method="get") + return self.send_command(f"/discovery/{uuid}", method="get") @_api_bool async def update_hass_api(self, http_config, refresh_token): @@ -156,7 +156,7 @@ async def send_command(self, command, method="post", payload=None, timeout=10): with async_timeout.timeout(timeout): request = await self.websession.request( method, - "http://{}{}".format(self._ip, command), + f"http://{self._ip}{command}", json=payload, headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, ) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index f42aaca44389d7..3b1b83745105ba 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -75,7 +75,7 @@ async def _command_proxy( method = getattr(self._websession, request.method.lower()) client = await method( - "http://{}/{}".format(self._host, path), + f"http://{self._host}/{path}", data=data, headers=headers, timeout=read_timeout, diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 84e2b0963624f8..4ecb9a8419f529 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -42,7 +42,7 @@ def __init__(self, host: str, websession: aiohttp.ClientSession): def _create_url(self, token: str, path: str) -> str: """Create URL to service.""" - return "http://{}/ingress/{}/{}".format(self._host, token, path) + return f"http://{self._host}/ingress/{token}/{path}" async def _handle( self, request: web.Request, token: str, path: str @@ -91,7 +91,7 @@ async def _handle_websocket( # Support GET query if request.query_string: - url = "{}?{}".format(url, request.query_string) + url = f"{url}?{request.query_string}" # Start proxy async with self._websession.ws_connect( @@ -175,15 +175,15 @@ def _init_header( headers[X_HASSIO] = os.environ.get("HASSIO_TOKEN", "") # Ingress information - headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(token) + headers[X_INGRESS_PATH] = f"/api/hassio_ingress/{token}" # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) if forward_for: - forward_for = "{}, {!s}".format(forward_for, connected_ip) + forward_for = f"{forward_for}, {connected_ip!s}" else: - forward_for = "{!s}".format(connected_ip) + forward_for = f"{connected_ip!s}" headers[hdrs.X_FORWARDED_FOR] = forward_for # Set X-Forwarded-Host diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5e474dafa076a8..a8aaa3390a73d3 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -133,12 +133,12 @@ def __init__( if host.startswith(("http://", "https://")): self.base_url = host elif use_ssl: - self.base_url = "https://{}".format(host) + self.base_url = f"https://{host}" else: - self.base_url = "http://{}".format(host) + self.base_url = f"http://{host}" if port is not None: - self.base_url += ":{}".format(port) + self.base_url += f":{port}" async def async_setup(hass, config): @@ -268,15 +268,11 @@ def register_view(self, view): if not hasattr(view, "url"): class_name = view.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "url"'.format(class_name) - ) + raise AttributeError(f'{class_name} missing required attribute "url"') if not hasattr(view, "name"): class_name = view.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "name"'.format(class_name) - ) + raise AttributeError(f'{class_name} missing required attribute "name"') view.register(self.app, self.app.router) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 71e7ff38924142..d8fa8853c7f18c 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -127,7 +127,7 @@ async def process_wrong_login(request): _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) hass.components.persistent_notification.async_create( - "Too many login attempts from {}".format(remote_addr), + f"Too many login attempts from {remote_addr}", "Banning IP address", NOTIFICATION_ID_BAN, ) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 634a96aa31235e..5945a4ca402fb8 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -43,9 +43,7 @@ async def wrapper(view, request, *args, **kwargs): kwargs["data"] = self._schema(data) except vol.Invalid as err: _LOGGER.error("Data does not match schema: %s", err) - return view.json_message( - "Message format incorrect: {}".format(err), 400 - ) + return view.json_message(f"Message format incorrect: {err}", 400) result = await method(view, request, *args, **kwargs) return result diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 76844407f7d112..952ca473fdc5f8 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -10,7 +10,7 @@ # mypy: allow-untyped-defs CACHE_TIME = 31 * 86400 # = 1 month -CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} +CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} # https://github.com/PyCQA/astroid/issues/633 diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 2564b8b31b41d8..007ed6517efe47 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -49,13 +49,11 @@ def _cv_input_number(cfg): maximum = cfg.get(CONF_MAX) if minimum >= maximum: raise vol.Invalid( - "Maximum ({}) is not greater than minimum ({})".format(minimum, maximum) + f"Maximum ({minimum}) is not greater than minimum ({maximum})" ) state = cfg.get(CONF_INITIAL) if state is not None and (state < minimum or state > maximum): - raise vol.Invalid( - "Initial value {} not in range {}-{}".format(state, minimum, maximum) - ) + raise vol.Invalid(f"Initial value {state} not in range {minimum}-{maximum}") return cfg diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2b7c7312f71cad..fc49bd65ced92f 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -45,12 +45,12 @@ def _cv_input_text(cfg): maximum = cfg.get(CONF_MAX) if minimum > maximum: raise vol.Invalid( - "Max len ({}) is not greater than min len ({})".format(minimum, maximum) + f"Max len ({minimum}) is not greater than min len ({maximum})" ) state = cfg.get(CONF_INITIAL) if state is not None and (len(state) < minimum or len(state) > maximum): raise vol.Invalid( - "Initial value {} length not in range {}-{}".format(state, minimum, maximum) + f"Initial value {state} length not in range {minimum}-{maximum}" ) return cfg diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index d24b70c4be01ce..236a996794a7ab 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -94,7 +94,7 @@ def __init__( self._state = 0 self._method = integration_method - self._name = name if name is not None else "{} integral".format(source_entity) + self._name = name if name is not None else f"{source_entity} integral" if unit_of_measurement is None: self._unit_template = "{}{}{}".format( diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 443a4cbc854a1b..75a0c0e8f976e6 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): for intent_type, conf in intents.items(): if CONF_ACTION in conf: conf[CONF_ACTION] = script.Script( - hass, conf[CONF_ACTION], "Intent Script {}".format(intent_type) + hass, conf[CONF_ACTION], f"Intent Script {intent_type}" ) intent.async_register(hass, ScriptIntentHandler(intent_type, conf)) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 715c06aca43c82..af0865bc685a21 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -113,7 +113,7 @@ def python_script_service_handler(call): @bind_hass def execute_script(hass, name, data=None): """Execute a script.""" - filename = "{}.py".format(name) + filename = f"{name}.py" with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil: source = fil.read() execute(hass, filename, source, data) @@ -166,9 +166,7 @@ def protected_getattr(obj, name, default=None): or isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME ): - raise ScriptError( - "Not allowed to access {}.{}".format(obj.__class__.__name__, name) - ) + raise ScriptError(f"Not allowed to access {obj.__class__.__name__}.{name}") return getattr(obj, name, default) @@ -188,7 +186,7 @@ def protected_getattr(obj, name, default=None): "_iter_unpack_sequence_": guarded_iter_unpack_sequence, "_unpack_sequence_": guarded_unpack_sequence, } - logger = logging.getLogger("{}.{}".format(__name__, filename)) + logger = logging.getLogger(f"{__name__}.{filename}") local = {"hass": hass, "data": data or {}, "logger": logger} try: diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index aee993fa10489c..3de0430d8f3898 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -107,7 +107,7 @@ def _drop_index(engine, table_name, index_name): # Engines like DB2/Oracle try: - engine.execute(text("DROP INDEX {index}".format(index=index_name))) + engine.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -170,7 +170,7 @@ def _add_columns(engine, table_name, columns_def): table_name, ) - columns_def = ["ADD {}".format(col_def) for col_def in columns_def] + columns_def = [f"ADD {col_def}" for col_def in columns_def] try: engine.execute( @@ -265,9 +265,7 @@ def _apply_update(engine, new_version, old_version): # 'context_parent_id CHARACTER(36)', # ]) else: - raise ValueError( - "No schema migration defined for version {}".format(new_version) - ) + raise ValueError(f"No schema migration defined for version {new_version}") def _inspect_schema_version(engine, session): diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index d810d50cfbf629..5a3223a8508f38 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -209,7 +209,7 @@ async def async_turn_on(self, **kwargs): await self.script.async_run(kwargs.get(ATTR_VARIABLES), context) except Exception as err: # pylint: disable=broad-except self.script.async_log_exception( - _LOGGER, "Error executing script {}".format(self.entity_id), err + _LOGGER, f"Error executing script {self.entity_id}", err ) raise err diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index c9bd486053e0b4..68561d45f8ff81 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -198,7 +198,7 @@ async def async_service_handler(service): return if service.service == "write": logger = logging.getLogger( - service.data.get(CONF_LOGGER, "{}.external".format(__name__)) + service.data.get(CONF_LOGGER, f"{__name__}.external") ) level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 77d24fd7aab2ae..3e7900502d6c37 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -165,9 +165,7 @@ async def async_say_handle(service): DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True ) - service_name = p_config.get( - CONF_SERVICE_NAME, "{}_{}".format(p_type, SERVICE_SAY) - ) + service_name = p_config.get(CONF_SERVICE_NAME, f"{p_type}_{SERVICE_SAY}") hass.services.async_register( DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY ) @@ -229,7 +227,7 @@ def init_tts_cache_dir(cache_dir): init_tts_cache_dir, cache_dir ) except OSError as err: - raise HomeAssistantError("Can't init cache dir {}".format(err)) + raise HomeAssistantError(f"Can't init cache dir {err}") def get_cache_files(): """Return a dict of given engine files.""" @@ -251,7 +249,7 @@ def get_cache_files(): try: cache_files = await self.hass.async_add_job(get_cache_files) except OSError as err: - raise HomeAssistantError("Can't read cache dir {}".format(err)) + raise HomeAssistantError(f"Can't read cache dir {err}") if cache_files: self.file_cache.update(cache_files) @@ -293,7 +291,7 @@ async def async_get_url( # Languages language = language or provider.default_language if language is None or language not in provider.supported_languages: - raise HomeAssistantError("Not supported language {0}".format(language)) + raise HomeAssistantError(f"Not supported language {language}") # Options if provider.default_options and options: @@ -308,9 +306,7 @@ async def async_get_url( if opt_name not in (provider.supported_options or []) ] if invalid_opts: - raise HomeAssistantError( - "Invalid options found: {}".format(invalid_opts) - ) + raise HomeAssistantError(f"Invalid options found: {invalid_opts}") options_key = ctypes.c_size_t(hash(frozenset(options))).value else: options_key = "-" @@ -330,7 +326,7 @@ async def async_get_url( engine, key, message, use_cache, language, options ) - return "{}/api/tts_proxy/{}".format(self.base_url, filename) + return f"{self.base_url}/api/tts_proxy/{filename}" async def async_get_tts_audio(self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. @@ -341,10 +337,10 @@ async def async_get_tts_audio(self, engine, key, message, cache, language, optio extension, data = await provider.async_get_tts_audio(message, language, options) if data is None or extension is None: - raise HomeAssistantError("No TTS from {} for '{}'".format(engine, message)) + raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = ("{}.{}".format(key, extension)).lower() + filename = (f"{key}.{extension}").lower() data = self.write_tags(filename, data, provider, message, language, options) @@ -381,7 +377,7 @@ async def async_file_to_mem(self, key): """ filename = self.file_cache.get(key) if not filename: - raise HomeAssistantError("Key {} not in file cache!".format(key)) + raise HomeAssistantError(f"Key {key} not in file cache!") voice_file = os.path.join(self.cache_dir, filename) @@ -394,7 +390,7 @@ def load_speech(): data = await self.hass.async_add_job(load_speech) except OSError: del self.file_cache[key] - raise HomeAssistantError("Can't read {}".format(voice_file)) + raise HomeAssistantError(f"Can't read {voice_file}") self._async_store_to_memcache(key, filename, data) @@ -425,7 +421,7 @@ async def async_read_tts(self, filename): if key not in self.mem_cache: if key not in self.file_cache: - raise HomeAssistantError("{} not in cache!".format(key)) + raise HomeAssistantError(f"{key} not in cache!") await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index c09c43dc2826b2..17eacc326d3b58 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -98,7 +98,7 @@ async def async_setup(hass, config): tariff_confs.append( { CONF_METER: meter, - CONF_NAME: "{} {}".format(meter, tariff), + CONF_NAME: f"{meter} {tariff}", CONF_TARIFF: tariff, } ) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1eceaea2ae54fc..1ad4300b28b74f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -107,7 +107,7 @@ def __init__( if name: self._name = name else: - self._name = "{} meter".format(source_entity) + self._name = f"{source_entity} meter" self._unit_of_measurement = None self._period = meter_type self._period_offset = meter_offset diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2ed03b73eff947..af107a6ae0df10 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -33,7 +33,7 @@ def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" - zeroconf_name = "{}.{}".format(hass.config.location_name, ZEROCONF_TYPE) + zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { "version": __version__, diff --git a/homeassistant/config.py b/homeassistant/config.py index 1f42b3db25e943..f4775e718054ee 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -317,7 +317,7 @@ def _load_hass_yaml_config() -> Dict: path = find_config_file(hass.config.config_dir) if path is None: raise HomeAssistantError( - "Config file not found in: {}".format(hass.config.config_dir) + f"Config file not found in: {hass.config.config_dir}" ) config = load_yaml_config_file(path) return config @@ -443,7 +443,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: This method must be run in the event loop. """ - message = "Invalid config for [{}]: ".format(domain) + message = f"Invalid config for [{domain}]: " if "extra keys not allowed" in ex.error_message: message += ( "[{option}] is an invalid option for [{domain}]. " @@ -705,7 +705,7 @@ async def merge_packages_config( error = _recursive_merge(conf=config[comp_name], package=comp_conf) if error: _log_pkg_error( - pack_name, comp_name, config, "has duplicate key '{}'".format(error) + pack_name, comp_name, config, f"has duplicate key '{error}'" ) return config @@ -777,7 +777,7 @@ async def async_process_component_config( p_config ) except vol.Invalid as ex: - async_log_exception(ex, "{}.{}".format(domain, p_name), p_config, hass) + async_log_exception(ex, f"{domain}.{p_name}", p_config, hass) continue platforms.append(p_validated) @@ -836,7 +836,7 @@ def async_notify_setup_error( else: part = name - message += " - {}\n".format(part) + message += f" - {part}\n" message += "\nPlease check your config." diff --git a/homeassistant/core.py b/homeassistant/core.py index e8e33a0479eb61..4d7596d667b106 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1365,7 +1365,7 @@ def set_time_zone(self, time_zone_str: str) -> None: self.time_zone = time_zone dt_util.set_default_time_zone(time_zone) else: - raise ValueError("Received invalid time zone {}".format(time_zone_str)) + raise ValueError(f"Received invalid time zone {time_zone_str}") @callback def _update( diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 0af6677dceb246..6bbd757fca63a6 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -126,7 +126,7 @@ async def _async_handle_step( self, flow: Any, step_id: str, user_input: Optional[Dict] ) -> Dict: """Handle a step of a flow.""" - method = "async_step_{}".format(step_id) + method = f"async_step_{step_id}" if not hasattr(flow, method): self._progress.pop(flow.flow_id) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index dfb001ff0d7976..89caf730ad7fa6 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -25,7 +25,7 @@ class TemplateError(HomeAssistantError): def __init__(self, exception: jinja2.TemplateError) -> None: """Init the error.""" - super().__init__("{}: {}".format(exception.__class__.__name__, exception)) + super().__init__(f"{exception.__class__.__name__}: {exception}") class PlatformNotReady(HomeAssistantError): @@ -73,10 +73,10 @@ class ServiceNotFound(HomeAssistantError): def __init__(self, domain: str, service: str) -> None: """Initialize error.""" - super().__init__(self, "Service {}.{} not found".format(domain, service)) + super().__init__(self, f"Service {domain}.{service} not found") self.domain = domain self.service = service def __str__(self) -> str: """Return string representation.""" - return "Unable to find service {}/{}".format(self.domain, self.service) + return f"Unable to find service {self.domain}/{self.service}" diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index bc39d5d5720c54..f49ae9768272ad 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -62,7 +62,7 @@ def _pack_error(package, component, config, message): message = "Package {} setup failed. Component {} {}".format( package, component, message ) - domain = "homeassistant.packages.{}.{}".format(package, component) + domain = f"homeassistant.packages.{package}.{component}" pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) @@ -77,9 +77,9 @@ def _comp_error(ex, domain, config): return result.add_error("File configuration.yaml not found.") config = await hass.async_add_executor_job(load_yaml_config_file, config_path) except FileNotFoundError: - return result.add_error("File not found: {}".format(config_path)) + return result.add_error(f"File not found: {config_path}") except HomeAssistantError as err: - return result.add_error("Error loading {}: {}".format(config_path, err)) + return result.add_error(f"Error loading {config_path}: {err}") finally: yaml_loader.clear_secret_cache() @@ -106,13 +106,13 @@ def _comp_error(ex, domain, config): try: integration = await async_get_integration_with_requirements(hass, domain) except (RequirementsNotFound, loader.IntegrationNotFound) as ex: - result.add_error("Component error: {} - {}".format(domain, ex)) + result.add_error(f"Component error: {domain} - {ex}") continue try: component = integration.get_component() except ImportError as ex: - result.add_error("Component error: {} - {}".format(domain, ex)) + result.add_error(f"Component error: {domain} - {ex}") continue config_schema = getattr(component, "CONFIG_SCHEMA", None) @@ -159,7 +159,7 @@ def _comp_error(ex, domain, config): RequirementsNotFound, ImportError, ) as ex: - result.add_error("Platform error {}.{} - {}".format(domain, p_name, ex)) + result.add_error(f"Platform error {domain}.{p_name} - {ex}") continue # Validate platform specific schema @@ -168,7 +168,7 @@ def _comp_error(ex, domain, config): try: p_validated = platform_schema(p_validated) except vol.Invalid as ex: - _comp_error(ex, "{}.{}".format(domain, p_name), p_validated) + _comp_error(ex, f"{domain}.{p_name}", p_validated) continue platforms.append(p_validated) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index bd96e1bafdb5f4..dc2e46cc6b22fe 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -243,11 +243,11 @@ async def async_update_ha_state(self, force_refresh=False): This method must be run in the event loop. """ if self.hass is None: - raise RuntimeError("Attribute hass is None for {}".format(self)) + raise RuntimeError(f"Attribute hass is None for {self}") if self.entity_id is None: raise NoEntitySpecifiedError( - "No entity id specified for entity {}".format(self.name) + f"No entity id specified for entity {self.name}" ) # update entity data @@ -264,11 +264,11 @@ async def async_update_ha_state(self, force_refresh=False): def async_write_ha_state(self): """Write the state to the state machine.""" if self.hass is None: - raise RuntimeError("Attribute hass is None for {}".format(self)) + raise RuntimeError(f"Attribute hass is None for {self}") if self.entity_id is None: raise NoEntitySpecifiedError( - "No entity id specified for entity {}".format(self.name) + f"No entity id specified for entity {self.name}" ) self._async_write_ha_state() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index b28beeaea72bba..a923763570285e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -205,7 +205,7 @@ def async_register_entity_service(self, name, schema, func, required_features=No async def handle_service(call): """Handle the service.""" - service_name = "{}.{}".format(self.domain, name) + service_name = f"{self.domain}.{name}" await self.hass.helpers.service.entity_service_call( self._platforms.values(), func, call, service_name, required_features ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4a6a3038fd0c97..7d5debd484d4e8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -133,7 +133,7 @@ async def _async_setup_platform(self, async_create_setup_task, tries=0): current_platform.set(self) logger = self.logger hass = self.hass - full_name = "{}.{}".format(self.domain, self.platform_name) + full_name = f"{self.domain}.{self.platform_name}" logger.info("Setting up %s", full_name) warn_task = hass.loop.call_later( @@ -357,7 +357,7 @@ async def _async_add_entity( "Not adding entity %s because it's disabled", entry.name or entity.name - or '"{} {}"'.format(self.platform_name, entity.unique_id), + or f'"{self.platform_name} {entity.unique_id}"', ) return @@ -386,12 +386,12 @@ async def _async_add_entity( # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): - raise HomeAssistantError("Invalid entity id: {}".format(entity.entity_id)) + raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") if ( entity.entity_id in self.entities or entity.entity_id in self.hass.states.async_entity_ids(self.domain) ): - msg = "Entity id already exists: {}".format(entity.entity_id) + msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: msg += ". Platform {} does not generate unique IDs".format( self.platform_name diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7d81f62fa1c051..3be00c859a7a41 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -166,9 +166,7 @@ def async_get_or_create( ) entity_id = self.async_generate_entity_id( - domain, - suggested_object_id or "{}_{}".format(platform, unique_id), - known_object_ids, + domain, suggested_object_id or f"{platform}_{unique_id}", known_object_ids ) if ( diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ffd5918810f87b..4fb0d94287c453 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -58,7 +58,7 @@ async def async_handle( handler = hass.data.get(DATA_KEY, {}).get(intent_type) # type: IntentHandler if handler is None: - raise UnknownIntent("Unknown intent {}".format(intent_type)) + raise UnknownIntent(f"Unknown intent {intent_type}") intent = Intent(hass, platform, intent_type, slots or {}, text_input) @@ -68,13 +68,11 @@ async def async_handle( return result except vol.Invalid as err: _LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err) - raise InvalidSlotInfo( - "Received invalid slot info for {}".format(intent_type) - ) from err + raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err except IntentHandleError: raise except Exception as err: - raise IntentUnexpectedError("Error handling {}".format(intent_type)) from err + raise IntentUnexpectedError(f"Error handling {intent_type}") from err class IntentError(HomeAssistantError): @@ -109,7 +107,7 @@ def async_match_state( state = _fuzzymatch(name, states, lambda state: state.name) if state is None: - raise IntentHandleError("Unable to find an entity called {}".format(name)) + raise IntentHandleError(f"Unable to find an entity called {name}") return state @@ -118,9 +116,7 @@ def async_match_state( def async_test_feature(state: State, feature: int, feature_name: str) -> None: """Test is state supports a feature.""" if state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & feature == 0: - raise IntentHandleError( - "Entity {} does not support {}".format(state.name, feature_name) - ) + raise IntentHandleError(f"Entity {state.name} does not support {feature_name}") class IntentHandler: diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 8b32b1355fa410..30b428a9e17991 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -20,7 +20,7 @@ def display_temp( # If the temperature is not a number this can cause issues # with Polymer components, so bail early there. if not isinstance(temperature, Number): - raise TypeError("Temperature is not a number: {}".format(temperature)) + raise TypeError(f"Temperature is not a number: {temperature}") # type ignore: https://github.com/python/mypy/issues/7207 if temperature_unit != ha_unit: # type: ignore diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ca320cb1c331b2..98e3849bfb6332 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -320,10 +320,10 @@ def __getattr__(self, name): """Return the domain state.""" if "." in name: if not valid_entity_id(name): - raise TemplateError("Invalid entity ID '{}'".format(name)) + raise TemplateError(f"Invalid entity ID '{name}'") return _get_state(self._hass, name) if not valid_entity_id(name + ".entity"): - raise TemplateError("Invalid domain name '{}'".format(name)) + raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._hass, name) def _collect_all(self): @@ -367,9 +367,9 @@ def __init__(self, hass, domain): def __getattr__(self, name): """Return the states.""" - entity_id = "{}.{}".format(self._domain, name) + entity_id = f"{self._domain}.{name}" if not valid_entity_id(entity_id): - raise TemplateError("Invalid entity ID '{}'".format(entity_id)) + raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_state(self._hass, entity_id) def _collect_domain(self): @@ -399,7 +399,7 @@ def __len__(self): def __repr__(self): """Representation of Domain States.""" - return "