From 9c689d757c5a9ea2d45cb0ed54e2ff8db93ea19c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 5 Jan 2023 21:38:24 +0100 Subject: [PATCH 01/16] Limit calls in UniFi to write state (#85248) Limit calls to write state to when relevant --- homeassistant/components/unifi/entity.py | 10 ++++++++-- homeassistant/components/unifi/sensor.py | 12 ++++++++++-- homeassistant/components/unifi/switch.py | 15 ++++++++++++--- homeassistant/components/unifi/update.py | 8 +++++++- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 79a80fad73c0..0809e81fe546 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.entity_description = description self._removed = False + self._write_state = False self._attr_available = description.available_fn(controller, obj_id) self._attr_device_info = description.device_info_fn(controller.api, obj_id) @@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return - self._attr_available = description.available_fn(self.controller, self._obj_id) + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + self._write_state = True self.async_update_state(event, obj_id) - self.async_write_ha_state() + if self._write_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e320d1a0d4e5..585221d05bb8 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -253,11 +253,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return + update_state = False + obj = description.object_fn(self.controller.api, self._obj_id) if (value := description.value_fn(self.controller, obj)) != self.native_value: self._attr_native_value = value - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() + update_state = True + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + update_state = True + if update_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 44007e4c1a83..1d83effba362 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -410,11 +410,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return + update_state = False + if not description.only_event_for_state_change: obj = description.object_fn(self.controller.api, self._obj_id) - self._attr_is_on = description.is_on_fn(self.controller.api, obj) - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() + if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on: + self._attr_is_on = is_on + update_state = True + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + update_state = True + if update_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 6cff6b7932d8..0810cbb780cc 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): description = self.entity_description obj = description.object_fn(self.controller.api, self._obj_id) - self._attr_in_progress = description.state_fn(self.controller.api, obj) + if ( + in_progress := description.state_fn(self.controller.api, obj) + ) != self.in_progress: + self._attr_in_progress = in_progress + self._write_state = True self._attr_installed_version = obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version + if self.installed_version != self.latest_version: + self._write_state = True From 8bf2299407a948c7e3becf4650a0ce28920ec221 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 6 Jan 2023 03:49:22 +0100 Subject: [PATCH 02/16] Only subscribe to relevant IDs for state updates (#85252) Make sure to only subscribe to the relevant ID --- homeassistant/components/unifi/sensor.py | 1 + homeassistant/components/unifi/switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 585221d05bb8..e017ff4ed0ee 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) self.async_on_remove( diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 1d83effba362..b0f042c78cd7 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) self.async_on_remove( From dcd07d313598b1c781da381372e6f1551a87c22b Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Wed, 4 Jan 2023 21:09:54 -0500 Subject: [PATCH 03/16] Bump pyeconet to 0.1.18 to fix energy usage (#85094) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 19455d8dffb7..8aed197cf4c2 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,7 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.17"], + "requirements": ["pyeconet==0.1.18"], "codeowners": ["@vangorra", "@w1ll1am23"], "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"] diff --git a/requirements_all.txt b/requirements_all.txt index 2777f4c52269..cf138171729d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75dd5bbf45da..941538838b05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pydexcom==0.2.3 pydroid-ipcam==2.0.0 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.efergy pyefergy==22.1.1 From fe89b663e77f7a46fd2921f3e9c3d41ddda80487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 5 Jan 2023 23:45:29 +0100 Subject: [PATCH 04/16] Fix lacrosse_view fetching of latest data (#85117) lacrosse_view: fixed fetching of latest data When using datetime.utcnow(), it only replaces timezone information with UTC making the actual time offset by the timezone. When you are in UTC- timezones, it makes no issue as the offset is in the future, but when in UTC+, the last hour(s) of data are missing. This commits swtiches to time.time() as UTC timestamp is actually what the API expects. It also reduces the window to one hour what noticeably improves the API performance. --- .../components/lacrosse_view/coordinator.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 5361f94d04f1..8dcbd8a2e5ea 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -1,7 +1,8 @@ """DataUpdateCoordinator for LaCrosse View.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta +from time import time from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor @@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): ) -> None: """Initialize DataUpdateCoordinator for LaCrosse View.""" self.api = api - self.last_update = datetime.utcnow() + self.last_update = time() self.username = entry.data["username"] self.password = entry.data["password"] self.hass = hass @@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): async def _async_update_data(self) -> list[Sensor]: """Get the data for LaCrosse View.""" - now = datetime.utcnow() + now = int(time()) - if self.last_update < now - timedelta(minutes=59): # Get new token + if self.last_update < now - 59 * 60: # Get new token once in a hour self.last_update = now try: await self.api.login(self.username, self.password) except LoginError as error: raise ConfigEntryAuthFailed from error - # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) - yesterday = now - timedelta(days=1) - yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) - yesterday_timestamp = datetime.timestamp(yesterday) - try: + # Fetch last hour of data sensors = await self.api.get_sensors( location=Location(id=self.id, name=self.name), tz=self.hass.config.time_zone, - start=str(int(yesterday_timestamp)), - end=str(int(datetime.timestamp(now))), + start=str(now - 3600), + end=str(now), ) except HTTPError as error: raise ConfigEntryNotReady from error From 563ad02c65264edeefddf941d4934bb28e828cac Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 5 Jan 2023 03:05:46 +0100 Subject: [PATCH 05/16] Bump bthome-ble to 2.4.1 (#85153) fix https://github.com/home-assistant/core/issues/85142 fixes undefined --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 7a879608fc4f..1be63f5f486a 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,7 +17,7 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.4.0"], + "requirements": ["bthome-ble==2.4.1"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index cf138171729d..7792d48739dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -488,7 +488,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 941538838b05..8b9827a2cd3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.buienradar buienradar==1.0.5 From 3c2b7c0d6951908f35fa7f33ff76a2f38a39b12e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Jan 2023 11:24:38 +0100 Subject: [PATCH 06/16] Bump hatasmota to 0.6.2 (#85182) --- homeassistant/components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6e3e69f59fea..df01f719cec1 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.6.1"], + "requirements": ["hatasmota==0.6.2"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 4b84bb8f86ab..74402a515868 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, @@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = { hc.LIGHT_LUX: LIGHT_LUX, hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS, hc.PERCENTAGE: PERCENTAGE, + hc.POWER_FACTOR: None, hc.POWER_WATT: UnitOfPower.WATT, hc.PRESSURE_HPA: UnitOfPressure.HPA, + hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE, hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR, diff --git a/requirements_all.txt b/requirements_all.txt index 7792d48739dd..682b8781edfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -858,7 +858,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b9827a2cd3c..c5be8d9c6d99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 From 8034faadcad20b570f7b167696368e5fd29941ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 17:02:17 +0100 Subject: [PATCH 07/16] Remove invalid AQI unit from Environment Canada (#85183) --- homeassistant/components/environment_canada/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index d3848086bf53..e7eceb8dadc7 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription( key="aqhi", name="AQHI", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, value_fn=_get_aqhi_value, ) From d73b86132b61e686944e12f36537b661ea71356f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:00:46 +0100 Subject: [PATCH 08/16] Adjust valid energy units (#85190) --- homeassistant/components/energy/sensor.py | 8 ++++---- homeassistant/components/energy/validate.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1509eb10afe3..5ad4c74a6cf0 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = { SensorStateClass.TOTAL_INCREASING, } VALID_ENERGY_UNITS: set[str] = { - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, } VALID_ENERGY_UNITS_GAS = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, *VALID_ENERGY_UNITS, } VALID_VOLUME_UNITS_WATER: set[str] = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index ea799fcdf060..55d11f5f04dd 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -22,10 +22,10 @@ from .const import DOMAIN ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, ) } ENERGY_PRICE_UNITS = tuple( @@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = ( ) GAS_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, + ), + sensor.SensorDeviceClass.GAS: ( + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, ), - sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), } GAS_PRICE_UNITS = tuple( f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units @@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price" WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,) WATER_USAGE_UNITS = { sensor.SensorDeviceClass.WATER: ( - UnitOfVolume.CUBIC_METERS, + UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, ), From 26ea02aa8ffe7f382bcd75c77a65a71eaf3439d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 21:30:52 +0100 Subject: [PATCH 09/16] Remove invalid device class for RSSI sensors (#85191) * Remove invalid device class for RRSI sensors * Restore state class --- homeassistant/components/zha/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index a66f3aa1fe87..ff95b66d1019 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False From 59d6f827c368ffb4298c1b27d128f9b64d88dc10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 20:28:13 +0100 Subject: [PATCH 10/16] Fix device class for DSMR gas sensors providing energy readings (#85202) --- homeassistant/components/dsmr/sensor.py | 16 +++++++ tests/components/dsmr/test_sensor.py | 55 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 36f734273f2f..a77577302d54 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + UnitOfEnergy, UnitOfVolume, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback @@ -591,6 +592,21 @@ class DSMREntity(SensorEntity): """Entity is only available if there is a telegram.""" return self.telegram is not None + @property + def device_class(self) -> SensorDeviceClass | None: + """Return the device class of this entity.""" + device_class = super().device_class + + # Override device class for gas sensors providing energy units, like + # kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas + with suppress(ValueError): + if device_class == SensorDeviceClass.GAS and UnitOfEnergy( + str(self.native_unit_of_measurement) + ): + return SensorDeviceClass.ENERGY + + return device_class + @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ee0ffa5db5fe..ac4b9587ec7f 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, VOLUME_CUBIC_METERS, + UnitOfEnergy, UnitOfPower, ) from homeassistant.helpers import entity_registry as er @@ -804,3 +805,57 @@ async def test_reconnect(hass, dsmr_connection_fixture): await hass.config_entries.async_unload(mock_entry.entry_id) assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED + + +async def test_gas_meter_providing_energy_reading(hass, dsmr_connection_fixture): + """Test that gas providing energy readings use the correct device class.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import GAS_METER_READING + from dsmr_parser.objects import MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(123.456), "unit": UnitOfEnergy.GIGA_JOULE}, + ] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + telegram_callback(telegram) + await asyncio.sleep(0) + + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") + assert gas_consumption.state == "123.456" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.GIGA_JOULE + ) From fa4c2500015cc1f78e589a15242ed298d03e3404 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 10:29:13 -1000 Subject: [PATCH 11/16] Improve error reporting when switchbot auth fails (#85244) * Improve error reporting when switchbot auth fails related issue #85243 * bump * coverage --- homeassistant/components/switchbot/config_flow.py | 6 +++++- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/strings.json | 2 +- .../components/switchbot/translations/en.json | 15 ++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/test_config_flow.py | 3 ++- 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 6ba0e4637188..933d8ac3c563 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the SwitchBot API auth step.""" errors = {} assert self._discovered_adv is not None + description_placeholders = {} if user_input is not None: try: key_details = await self.hass.async_add_executor_job( @@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ) except SwitchbotAccountConnectionError as ex: raise AbortFlow("cannot_connect") from ex - except SwitchbotAuthenticationError: + except SwitchbotAuthenticationError as ex: + _LOGGER.debug("Authentication failed: %s", ex, exc_info=True) errors = {"base": "auth_failed"} + description_placeholders = {"error_detail": str(ex)} else: return await self.async_step_lock_key(key_details) @@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ), description_placeholders={ "name": name_from_discovery(self._discovered_adv), + **description_placeholders, }, ) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c7c50e5cf6e8..c38573f82ca5 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.36.1"], + "requirements": ["PySwitchbot==0.36.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 08fd960334af..c25769bee411 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -40,7 +40,7 @@ }, "error": { "encryption_key_invalid": "Key ID or Encryption key is invalid", - "auth_failed": "Authentication failed" + "auth_failed": "Authentication failed: {error_detail}" }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index b5658e58d6bf..d7065138051b 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -8,7 +8,7 @@ "unknown": "Unexpected error" }, "error": { - "auth_failed": "Authentication failed", + "auth_failed": "Authentication failed: {error_detail}", "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", @@ -47,18 +47,7 @@ "data": { "address": "Device address" } - }, - "lock_key": { - "description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.", - "data": { - "key_id": "Key ID", - "encryption_key": "Encryption key" } - } - }, - "error": { - "key_id_invalid": "Key ID or Encryption key is invalid", - "encryption_key_invalid": "Key ID or Encryption key is invalid" } }, "options": { @@ -70,4 +59,4 @@ } } } -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 682b8781edfe..217d9dddc0b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5be8d9c6d99..7f856633ed77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 1a3db48f1929..a8cccbeb31d1 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -481,7 +481,7 @@ async def test_user_setup_wolock_auth(hass): with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=SwitchbotAuthenticationError, + side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} + assert "error from api" in result["description_placeholders"]["error_detail"] with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", From ee88f34a91b9caad6624a0c9b1ba963cb248eb29 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 5 Jan 2023 22:06:40 +0100 Subject: [PATCH 12/16] bump reolink-aio to 0.1.2 (#85247) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 2c0deafca452..9ea4422203b6 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.1"], + "requirements": ["reolink-aio==0.1.2"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 217d9dddc0b2..477825e08221 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f856633ed77..e7ef6e1483f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 From edfd83c3a70cc810e2c5da18d0cfd7b28ed27920 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 6 Jan 2023 03:05:37 +0100 Subject: [PATCH 13/16] Bump bimmer_connected to 0.12.0 (#85255) * Bump bimmer_connected to 0.12.0 * Fix mypy * Remove not needed code Co-authored-by: rikroe --- .../bmw_connected_drive/device_tracker.py | 2 ++ .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bmw_connected_drive/__init__.py | 18 ------------------ 5 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c94d9b5b678a..12d297361832 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[0] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) @@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[1] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 98b6861fd49f..c03bdf6a26f9 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.4"], + "requirements": ["bimmer_connected==0.12.0"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 477825e08221..20c80512c2dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7ef6e1483f1..f1bbc843baaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bluetooth bleak-retry-connector==2.13.0 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index c2bb65b3fa74..81b3bb9fff37 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -4,7 +4,6 @@ import json from pathlib import Path from bimmer_connected.account import MyBMWAccount -from bimmer_connected.api.utils import log_to_to_file from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( @@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: } fetched_at = utcnow() - # simulate storing fingerprints - if account.config.log_response_path: - for brand in ["bmw", "mini"]: - log_to_to_file( - json.dumps(vehicles[brand]), - account.config.log_response_path, - f"vehicles_v2_{brand}", - ) - # Create a vehicle with base + specific state as provided by state/VIN API for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: vehicle_state_path = ( @@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: fetched_at, ) - # simulate storing fingerprints - if account.config.log_response_path: - log_to_to_file( - json.dumps(vehicle_state), - account.config.log_response_path, - f"state_{vehicle_base['vin']}", - ) - async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: """Mock a fully setup config entry and all components based on fixtures.""" From 2840821594fda614c2f076315bbdb3b5d3f7179e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 13:44:10 -1000 Subject: [PATCH 14/16] Reject the WiFI AP when considering to update a shelly config entry from zeroconf (#85265) Reject the WiFI AP IP when considering to update a shelly config entry from zeroconf fixes #85180 --- .../components/shelly/config_flow.py | 14 +++++++++- tests/components/shelly/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 8679edf53824..70d2c2492e88 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [ selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), ] +INTERNAL_WIFI_AP_IP = "192.168.33.1" + async def validate_input( hass: HomeAssistant, @@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): current_entry := await self.async_set_unique_id(mac) ) and current_entry.data[CONF_HOST] == host: await async_reconnect_soon(self.hass, current_entry) - self._abort_if_unique_id_configured({CONF_HOST: host}) + if host == INTERNAL_WIFI_AP_IP: + # If the device is broadcasting the internal wifi ap ip + # we can't connect to it, so we should not update the + # entry with the new host as it will be unreachable + # + # This is a workaround for a bug in the firmware 0.12 (and older?) + # which should be removed once the firmware is fixed + # and the old version is no longer in use + self._abort_if_unique_id_configured() + else: + self._abort_if_unique_id_configured({CONF_HOST: host}) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 6795049a2071..1c0a32853e13 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Shelly config flow.""" from __future__ import annotations +from dataclasses import replace from unittest.mock import AsyncMock, Mock, patch from aioshelly.exceptions import ( @@ -12,6 +13,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.shelly import config_flow from homeassistant.components.shelly.const import ( CONF_BLE_SCANNER_MODE, DOMAIN, @@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_with_wifi_ap_ip(hass): + """Test we ignore the Wi-FI AP IP.""" + + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "2.2.2.2"} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=replace(DISCOVERY_INFO, host=config_flow.INTERNAL_WIFI_AP_IP), + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Test config entry was not updated with the wifi ap ip + assert entry.data["host"] == "2.2.2.2" + + async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): From f0f2c12d91f7d3b71b98f18652d418c3343b1e53 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 5 Jan 2023 22:10:41 -0500 Subject: [PATCH 15/16] Fix Fully Kiosk service call config entry handling (#85275) * Make sure we're getting the fully_kiosk config entry * Make sure we're getting the fully_kiosk config entry --- .../components/fully_kiosk/services.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 3d63bf4f23c6..de269af891aa 100644 --- a/homeassistant/components/fully_kiosk/services.py +++ b/homeassistant/components/fully_kiosk/services.py @@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None: for target in call.data[ATTR_DEVICE_ID]: device = registry.async_get(target) if device: - coordinator = hass.data[DOMAIN][list(device.config_entries)[0]] - # fully_method(coordinator.fully, *args, **kwargs) would make - # test_services.py fail. - await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs) + for key in device.config_entries: + entry = hass.config_entries.async_get_entry(key) + if not entry: + continue + if entry.domain != DOMAIN: + continue + coordinator = hass.data[DOMAIN][key] + # fully_method(coordinator.fully, *args, **kwargs) would make + # test_services.py fail. + await getattr(coordinator.fully, fully_method.__name__)( + *args, **kwargs + ) + break async def async_load_url(call: ServiceCall) -> None: """Load a URL on the Fully Kiosk Browser.""" From 33bb9c230bef79c8563d36f489c31863677c03af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Jan 2023 22:24:17 -0500 Subject: [PATCH 16/16] Bumped version to 2023.1.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a606656f5790..7d65c6b44a30 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 7d577d967058..4bb8a5f29118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0" +version = "2023.1.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"