diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c94d9b5b678..12d29736183 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 98b6861fd49..c03bdf6a26f 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/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 7a879608fc4..1be63f5f486 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/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 36f734273f2..a77577302d5 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/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 19455d8dffb..8aed197cf4c 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/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1509eb10afe..5ad4c74a6cf 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 ea799fcdf06..55d11f5f04d 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, ), diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index d3848086bf5..e7eceb8dadc 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, ) diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 3d63bf4f23c..de269af891a 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.""" diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 5361f94d04f..8dcbd8a2e5e 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 diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 2c0deafca45..9ea4422203b 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/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 8679edf5382..70d2c2492e8 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/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 6ba0e463718..933d8ac3c56 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 c7c50e5cf6e..c38573f82ca 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 08fd960334a..c25769bee41 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 b5658e58d6b..d7065138051 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/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6e3e69f59fe..df01f719cec 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 4b84bb8f86a..74402a51586 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/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 79a80fad73c..0809e81fe54 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 e320d1a0d4e..e017ff4ed0e 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( @@ -253,11 +254,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 44007e4c1a8..b0f042c78cd 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( @@ -410,11 +411,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 6cff6b7932d..0810cbb780c 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 diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index a66f3aa1fe8..ff95b66d101 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 diff --git a/homeassistant/const.py b/homeassistant/const.py index a606656f579..7d65c6b44a3 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 7d577d96705..4bb8a5f2911 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" diff --git a/requirements_all.txt b/requirements_all.txt index 2777f4c5226..20c80512c2d 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 75dd5bbf45d..f1bbc843baa 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index c2bb65b3fa7..81b3bb9fff3 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.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ee0ffa5db5f..ac4b9587ec7 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 + ) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 6795049a207..1c0a32853e1 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): diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 1a3db48f192..a8cccbeb31d 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",