diff --git a/Dockerfile b/Dockerfile index 647c2b8ac07..4646e9f01f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,15 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +ENV \ + S6_SERVICES_GRACETIME=60000 + WORKDIR /usr/src ## Setup Home Assistant COPY . homeassistant/ -RUN pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ +RUN \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c46bd754319..4fdad670f09 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -6,6 +6,7 @@ from zlib import adler32 import voluptuous as vol from homeassistant.components import cover +from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -200,7 +201,7 @@ def get_accessory(hass, driver, state, aid, config): device_class = state.attributes.get(ATTR_DEVICE_CLASS) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if device_class == "garage" and features & ( + if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE ): a_type = "GarageDoorOpener" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index b0c49a58a6a..d80fd3c5338 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,6 +2,6 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.1"], + "requirements": ["HAP-python==2.8.2"], "codeowners": [] } diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index fe0808414ad..b7239c8bf49 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -116,7 +116,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("IPP Parse Error") return self.async_abort(reason="parse_error") - self.discovery_info[CONF_UUID] = info[CONF_UUID] + if info[CONF_UUID] is not None: + self.discovery_info[CONF_UUID] = info[CONF_UUID] await self.async_set_unique_id(self.discovery_info[CONF_UUID]) self._abort_if_unique_id_configured( diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 4be57f13fbb..0cb7c108b63 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.9.1"], + "requirements": ["pyipp==0.9.2"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e69ea352c8e..2102a2a8225 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.8.0"], + "requirements": ["nexia==0.8.2"], "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index a0bfbab9b4f..6ec06a10988 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -411,8 +411,11 @@ class ONVIFHassCamera(Camera): req = media_service.create_type("GetSnapshotUri") req.ProfileToken = profiles[self._profile_index].token - snapshot_uri = await media_service.GetSnapshotUri(req) - self._snapshot = snapshot_uri.Uri + try: + snapshot_uri = await media_service.GetSnapshotUri(req) + self._snapshot = snapshot_uri.Uri + except ServerDisconnectedError as err: + _LOGGER.debug("Camera does not support GetSnapshotUri: %s", err) _LOGGER.debug( "ONVIF Camera Using the following URL for %s snapshot: %s", diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 329b26221b8..3b73caecacd 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -129,7 +129,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): @property def is_on(self): - """Get the current value in kWh.""" + """Grid is online.""" return ( self._coordinator.data[POWERWALL_API_GRID_STATUS] == POWERWALL_GRID_ONLINE ) diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 2e9c3739c48..d05e42f6bf7 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -2,12 +2,10 @@ DOMAIN = "powerwall" -POWERWALL_SITE_NAME = "site_name" - POWERWALL_OBJECT = "powerwall" POWERWALL_COORDINATOR = "coordinator" -UPDATE_INTERVAL = 60 +UPDATE_INTERVAL = 30 ATTR_REGION = "region" ATTR_GRID_CODE = "grid_code" @@ -46,3 +44,5 @@ POWERWALL_RUNNING_KEY = "running" MODEL = "PowerWall 2" MANUFACTURER = "Tesla" + +ENERGY_KILO_WATT = "kW" diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index cf49b36a570..72dbd38a418 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -4,7 +4,6 @@ import logging from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, UNIT_PERCENTAGE, ) @@ -14,6 +13,7 @@ from .const import ( ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, DOMAIN, + ENERGY_KILO_WATT, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, POWERWALL_API_METERS, @@ -87,7 +87,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return ENERGY_KILO_WATT_HOUR + return ENERGY_KILO_WATT @property def name(self): @@ -106,7 +106,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def state(self): - """Get the current value in kWh.""" + """Get the current value in kW.""" meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] return round(float(meter.instant_power / 1000), 3) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2af35e8fb92..9e59b63adb4 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -32,6 +32,7 @@ from .core.const import ( SIGNAL_ADD_ENTITIES, RadioType, ) +from .core.discovery import GROUP_PROBE DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) @@ -138,6 +139,7 @@ async def async_unload_entry(hass, config_entry): """Unload ZHA config entry.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() + GROUP_PROBE.cleanup() api.async_unload_api(hass) dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, []) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index da151f67dbb..fe139a8239b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -208,6 +208,7 @@ SIGNAL_SET_LEVEL = "set_level" SIGNAL_STATE_ATTR = "update_state_attribute" SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" SIGNAL_REMOVE_GROUP = "remove_group" +SIGNAL_GROUP_ENTITY_REMOVED = "group_entity_removed" SIGNAL_GROUP_MEMBERSHIP_CHANGE = "group_membership_change" UNKNOWN = "unknown" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index ad3d1ff18ad..0215858721f 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -551,7 +551,15 @@ class ZHADevice(LogMixin): async def async_remove_from_group(self, group_id): """Remove this device from the provided zigbee group.""" - await self._zigpy_device.remove_from_group(group_id) + try: + await self._zigpy_device.remove_from_group(group_id) + except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove device '%s' from group: 0x%04x ex: %s", + self._zigpy_device.ieee, + group_id, + str(ex), + ) async def async_bind_to_group(self, group_id, cluster_bindings): """Directly bind this device to a group for the given clusters.""" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 90ec0e6e250..4540c9158de 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,7 +6,10 @@ from typing import Callable, List, Tuple from homeassistant import const as ha_const from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import HomeAssistantType @@ -166,10 +169,30 @@ class GroupProbe: def __init__(self): """Initialize instance.""" self._hass = None + self._unsubs = [] def initialize(self, hass: HomeAssistantType) -> None: """Initialize the group probe.""" self._hass = hass + self._unsubs.append( + async_dispatcher_connect( + hass, zha_const.SIGNAL_GROUP_ENTITY_REMOVED, self._reprobe_group + ) + ) + + def cleanup(self): + """Clean up on when zha shuts down.""" + for unsub in self._unsubs[:]: + unsub() + self._unsubs.remove(unsub) + + def _reprobe_group(self, group_id: int) -> None: + """Reprobe a group for entities after its members change.""" + zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_group = zha_gateway.groups.get(group_id) + if zha_group is None: + return + self.discover_group_entities(zha_group) @callback def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e032de4d94c..e97e2185dc5 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -20,7 +20,10 @@ from homeassistant.helpers.device_registry import ( async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry as get_ent_reg, +) from . import discovery, typing as zha_typing from .const import ( @@ -77,7 +80,7 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import ZHAGroup from .patches import apply_application_controller_patch -from .registries import RADIO_TYPES +from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES from .store import async_get_registry from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType @@ -273,6 +276,9 @@ class ZHAGateway: async_dispatcher_send( self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) + if len(zha_group.members) == 2: + # we need to do this because there wasn't already a group entity to remove and re-add + discovery.GROUP_PROBE.discover_group_entities(zha_group) def group_added(self, zigpy_group: ZigpyGroupType) -> None: """Handle zigpy group added event.""" @@ -289,6 +295,7 @@ class ZHAGateway: async_dispatcher_send( self._hass, f"{SIGNAL_REMOVE_GROUP}_0x{zigpy_group.group_id:04x}" ) + self._cleanup_group_entity_registry_entries(zigpy_group) def _send_group_gateway_message( self, zigpy_group: ZigpyGroupType, gateway_message_type: str @@ -368,6 +375,35 @@ class ZHAGateway: e for e in entity_refs if e.reference_id != entity.entity_id ] + def _cleanup_group_entity_registry_entries( + self, zigpy_group: ZigpyGroupType + ) -> None: + """Remove entity registry entries for group entities when the groups are removed from HA.""" + # first we collect the potential unique ids for entities that could be created from this group + possible_entity_unique_ids = [ + f"{domain}_zha_group_0x{zigpy_group.group_id:04x}" + for domain in GROUP_ENTITY_DOMAINS + ] + + # then we get all group entity entries tied to the coordinator + all_group_entity_entries = async_entries_for_device( + self.ha_entity_registry, self.coordinator_zha_device.device_id + ) + + # then we get the entity entries for this specific group by getting the entries that match + entries_to_remove = [ + entry + for entry in all_group_entity_entries + if entry.unique_id in possible_entity_unique_ids + ] + + # then we remove the entries from the entity registry + for entry in entries_to_remove: + _LOGGER.debug( + "cleaning up entity registry entry for entity: %s", entry.entity_id + ) + self.ha_entity_registry.async_remove(entry.entity_id) + @property def devices(self): """Return devices.""" @@ -557,15 +593,7 @@ class ZHAGateway: ) tasks.append(self.devices[ieee].async_add_to_group(group_id)) await asyncio.gather(*tasks) - zha_group = self.groups.get(group_id) - _LOGGER.debug( - "Probing group: %s:0x%04x for entity discovery", - zha_group.name, - zha_group.group_id, - ) - discovery.GROUP_PROBE.discover_group_entities(zha_group) - - return zha_group + return self.groups.get(group_id) async def async_remove_zigpy_group(self, group_id: int) -> None: """Remove a Zigbee group from Zigpy.""" diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 2d098d60bfb..d28bc622bbe 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -8,7 +8,10 @@ from typing import Any, Awaitable, Dict, List, Optional from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity @@ -19,6 +22,7 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, + SIGNAL_GROUP_ENTITY_REMOVED, SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, SIGNAL_REMOVE_GROUP, @@ -32,7 +36,7 @@ ENTITY_SUFFIX = "entity_suffix" RESTART_GRACE_PERIOD = 7200 # 2 hours -class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): +class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs): @@ -112,7 +116,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): @callback def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: """Set the entity state.""" - pass async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -133,11 +136,6 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): self.zha_device.gateway.remove_entity_reference(self) self.remove_future.set_result(True) - @callback - def async_restore_last_state(self, last_state) -> None: - """Restore previous state.""" - pass - async def async_accept_signal( self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False ): @@ -158,7 +156,7 @@ class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): _LOGGER.log(level, msg, *args) -class ZhaEntity(BaseZhaEntity): +class ZhaEntity(BaseZhaEntity, RestoreEntity): """A base class for non group ZHA entities.""" def __init__( @@ -181,6 +179,13 @@ class ZhaEntity(BaseZhaEntity): async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() + self.remove_future = asyncio.Future() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE}_{self.zha_device.ieee}", + self.async_remove, + signal_override=True, + ) await self.async_check_recently_seen() await self.async_accept_signal( None, @@ -197,6 +202,16 @@ class ZhaEntity(BaseZhaEntity): self.remove_future, ) + async def async_will_remove_from_hass(self) -> None: + """Disconnect entity object when removed.""" + await super().async_will_remove_from_hass() + self.zha_device.gateway.remove_entity_reference(self) + self.remove_future.set_result(True) + + @callback + def async_restore_last_state(self, last_state) -> None: + """Restore previous state.""" + async def async_check_recently_seen(self) -> None: """Check if the device was seen within the last 2 hours.""" last_state = await self.async_get_last_state() @@ -246,13 +261,20 @@ class ZhaGroupEntity(BaseZhaEntity): await self.async_accept_signal( None, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", - self._update_group_entities, + self.async_remove, signal_override=True, ) self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, self.async_state_changed_listener ) + + def send_removed_signal(): + async_dispatcher_send( + self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id + ) + + self.async_on_remove(send_removed_signal) await self.async_update() @callback @@ -262,17 +284,6 @@ class ZhaGroupEntity(BaseZhaEntity): """Handle child updates.""" self.async_schedule_update_ha_state(True) - def _update_group_entities(self): - """Update tracked entities when membership changes.""" - group = self.zha_device.gateway.get_group(self._group_id) - self._entity_ids = group.get_domain_entity_ids(self.platform.domain) - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, self.async_state_changed_listener - ) - async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/const.py b/homeassistant/const.py index fb09e467c47..17af8a6e320 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 0a121a00178..3d4d5ad1394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.1 +HAP-python==2.8.2 # homeassistant.components.mastodon Mastodon.py==1.5.0 @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.8.0 +nexia==0.8.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.1 +pyipp==0.9.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f64d2f7c948..9e128f83ca1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.1 +HAP-python==2.8.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.8.0 +nexia==0.8.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.1 +pyipp==0.9.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 3afed0ca8d8..d039fc04c86 100644 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -1,7 +1,8 @@ -#!/usr/bin/execlineb -S0 +#!/usr/bin/execlineb -S1 # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== if { s6-test ${1} -ne 100 } +if { s6-test ${1} -ne 256 } -s6-svscanctl -t /var/run/s6/services \ No newline at end of file +s6-svscanctl -t /var/run/s6/services diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 7f092683b7c..5d21a11d4b4 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -39,13 +39,14 @@ async def test_sensors(hass): "energy_exported": 10429451.9916853, "energy_imported": 4824191.60668611, "instant_average_voltage": 120.650001525879, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Site Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_load_now") assert state.state == "1.971" @@ -54,13 +55,14 @@ async def test_sensors(hass): "energy_exported": 1056797.48917483, "energy_imported": 4692987.91889705, "instant_average_voltage": 120.650001525879, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Load Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_battery_now") assert state.state == "-8.55" @@ -69,13 +71,14 @@ async def test_sensors(hass): "energy_exported": 3620010, "energy_imported": 4216170, "instant_average_voltage": 240.56, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Battery Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_solar_now") assert state.state == "10.49" @@ -84,13 +87,14 @@ async def test_sensors(hass): "energy_exported": 9864205.82222448, "energy_imported": 28177.5358355867, "instant_average_voltage": 120.685001373291, - "unit_of_measurement": "kWh", + "unit_of_measurement": "kW", "friendly_name": "Powerwall Solar Now", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value state = hass.states.get("sensor.powerwall_charge") assert state.state == "47.32" @@ -101,4 +105,5 @@ async def test_sensors(hass): } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) + for key, value in expected_attributes.items(): + assert state.attributes[key] == value diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 9bdd4966a4a..f297bfa5bf0 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -539,3 +539,21 @@ async def async_test_zha_group_light_entity( await zha_group.async_add_members([device_light_3.ieee]) await dev3_cluster_on_off.on() assert hass.states.get(entity_id).state == STATE_ON + + # make the group have only 1 member and now there should be no entity + await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee]) + assert len(zha_group.members) == 1 + assert hass.states.get(entity_id).state is None + # make sure the entity registry entry is still there + assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + + # add a member back and ensure that the group entity was created again + await zha_group.async_add_members([device_light_3.ieee]) + await dev3_cluster_on_off.on() + assert hass.states.get(entity_id).state == STATE_ON + + # remove the group and ensure that there is no entity and that the entity registry is cleaned up + assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + await zha_gateway.async_remove_zigpy_group(zha_group.group_id) + assert hass.states.get(entity_id).state is None + assert zha_gateway.ha_entity_registry.async_get(entity_id) is None