diff --git a/CODEOWNERS b/CODEOWNERS index e1dcf3d7dc9..29a22efa45f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -452,7 +452,7 @@ homeassistant/components/samsungtv/* @escoand @chemelli74 homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff -homeassistant/components/screenlogic/* @dieselrabbit +homeassistant/components/screenlogic/* @dieselrabbit @bdraco homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core homeassistant/components/select/* @home-assistant/core diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 474e69db435..ff6a4f5adb6 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -245,6 +245,15 @@ class AugustData(AugustSubscriberMixin): device_id, ) + async def async_lock_async(self, device_id): + """Lock the device but do not wait for a response since it will come via pubnub.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_lock_async, + self._august_gateway.access_token, + device_id, + ) + async def async_unlock(self, device_id): """Unlock the device.""" return await self._async_call_api_op_requires_bridge( @@ -254,6 +263,15 @@ class AugustData(AugustSubscriberMixin): device_id, ) + async def async_unlock_async(self, device_id): + """Unlock the device but do not wait for a response since it will come via pubnub.""" + return await self._async_call_api_op_requires_bridge( + device_id, + self._api.async_unlock_async, + self._august_gateway.access_token, + device_id, + ) + async def _async_call_api_op_requires_bridge( self, device_id, func, *args, **kwargs ): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index cf34952309b..541b5e94276 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -61,6 +61,17 @@ def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: return _activity_time_based_state(latest) +def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_IMAGE_CAPTURE} + ) + + if latest is None: + return False + + return _activity_time_based_state(latest) + + def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_DING} @@ -126,6 +137,13 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( value_fn=_retrieve_motion_state, is_time_based=True, ), + AugustBinarySensorEntityDescription( + key="doorbell_image_capture", + name="Image Capture", + icon="mdi:file-image", + value_fn=_retrieve_image_capture_state, + is_time_based=True, + ), AugustBinarySensorEntityDescription( key="doorbell_online", name="Online", diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 6f9ecf1b182..6c1f31c4b9c 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -63,7 +63,8 @@ class AugustCamera(AugustEntityMixin, Camera): def _update_from_data(self): """Get the latest state of the sensor.""" doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.DOORBELL_MOTION} + self._device_id, + {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}, ) if doorbell_activity is not None: diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 665b0036557..ea977a3c2d0 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -41,10 +41,16 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_lock(self, **kwargs): """Lock the device.""" + if self._data.activity_stream.pubnub.connected: + await self._data.async_lock_async(self._device_id) + return await self._call_lock_operation(self._data.async_lock) async def async_unlock(self, **kwargs): """Unlock the device.""" + if self._data.activity_stream.pubnub.connected: + await self._data.async_unlock_async(self._device_id) + return await self._call_lock_operation(self._data.async_unlock) async def _call_lock_operation(self, lock_operation): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fc365102926..c08f25177cc 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.13"], + "requirements": ["yalexs==1.1.17"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b084540bebb..b1a4cd0b358 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.2"], + "requirements": ["pychromecast==10.2.3"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index cd682057266..2fbb1c447ae 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -21,6 +21,7 @@ from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.websocket_api import const as ws_const +from homeassistant.util.location import async_detect_location_info from .const import ( DOMAIN, @@ -220,8 +221,23 @@ class CloudRegisterView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] + client_metadata = None + + if location_info := await async_detect_location_info( + hass.helpers.aiohttp_client.async_get_clientsession() + ): + client_metadata = { + "NC_COUNTRY_CODE": location_info.country_code, + "NC_REGION_CODE": location_info.region_code, + "NC_ZIP_CODE": location_info.zip_code, + } + async with async_timeout.timeout(REQUEST_TIMEOUT): - await cloud.auth.async_register(data["email"], data["password"]) + await cloud.auth.async_register( + data["email"], + data["password"], + client_metadata=client_metadata, + ) return self.json_message("ok") diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 517aa887a30..0bb00cd5ced 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.50.0"], + "requirements": ["hass-nabucasa==0.51.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 6703065476e..e1815b1d145 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.32"], + "requirements": ["flux_led==0.27.45"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c1d833ac169..b1f44c79414 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211229.0" + "home-assistant-frontend==20211229.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index d1b1073ebad..94817890160 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.8"], + "requirements": ["aioharmony==0.2.9"], "codeowners": [ "@ehendrix23", "@bramkragten", diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index cfa734559fc..6f2f09f3974 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -732,7 +732,12 @@ class HomeKit: """Remove all pairings for an accessory so it can be repaired.""" state = self.driver.state for client_uuid in list(state.paired_clients): - state.remove_paired_client(client_uuid) + # We need to check again since removing a single client + # can result in removing all the clients that the client + # granted access to if it was an admin, otherwise + # remove_paired_client can generate a KeyError + if client_uuid in state.paired_clients: + state.remove_paired_client(client_uuid) self.driver.async_persist() self.driver.async_update_advertisement() self._async_show_setup_message() diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0d8bf967c5b..34c62b31d2a 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -446,15 +446,25 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_advanced() entity_filter = self.hk_options.get(CONF_FILTER, {}) + entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) + all_supported_entities = _async_get_matching_entities( self.hass, domains=self.hk_options[CONF_DOMAINS], ) - data_schema = {} - entity_schema = vol.In - entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: + # Strip out entities that no longer exist to prevent error in the UI + valid_entities = [ + entity_id for entity_id in entities if entity_id in all_supported_entities + ] + if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: + # In accessory mode we can only have one + default_value = valid_entities[0] if valid_entities else None + entity_schema = vol.In + entities_schema_required = vol.Required + else: + # Bridge mode + entities_schema_required = vol.Optional include_exclude_mode = MODE_INCLUDE if not entities: include_exclude_mode = MODE_EXCLUDE @@ -463,13 +473,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Required(CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode) ] = vol.In(INCLUDE_EXCLUDE_MODES) entity_schema = cv.multi_select + default_value = valid_entities - # Strip out entities that no longer exist to prevent error in the UI - valid_entities = [ - entity_id for entity_id in entities if entity_id in all_supported_entities - ] data_schema[ - vol.Optional(CONF_ENTITIES, default=valid_entities) + entities_schema_required(CONF_ENTITIES, default=default_value) ] = entity_schema(all_supported_entities) return self.async_show_form( diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 29a0bc40ed6..bcf51f1966d 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -22,8 +22,8 @@ from .const import ( DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMAIN, - MODELS_V2, - ORBI_PORT, + MODELS_PORT_80, + PORT_80, ) from .errors import CannotLoginException from .router import get_api @@ -141,13 +141,13 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates=updated_data) updated_data[CONF_PORT] = DEFAULT_PORT - for model in MODELS_V2: + for model in MODELS_PORT_80: if discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( model ) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith( model ): - updated_data[CONF_PORT] = ORBI_PORT + updated_data[CONF_PORT] = PORT_80 self.placeholders.update(updated_data) self.discovered = True diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index cba2d7ff875..81fdf1d59e2 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -10,8 +10,8 @@ CONF_CONSIDER_HOME = "consider_home" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" -# update method V2 models -MODELS_V2 = [ +# models using port 80 instead of 5000 +MODELS_PORT_80 = [ "Orbi", "RBK", "RBR", @@ -29,7 +29,25 @@ MODELS_V2 = [ "SXR", "SXS", ] -ORBI_PORT = 80 +PORT_80 = 80 +# update method V2 models +MODELS_V2 = [ + "Orbi", + "RBK", + "RBR", + "RBS", + "RBW", + "LBK", + "LBR", + "CBK", + "CBR", + "SRC", + "SRK", + "SRS", + "SXK", + "SXR", + "SXS", +] # Icons DEVICE_ICONS = { diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 40e26128d8d..dbfd0439a85 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -149,6 +149,14 @@ class NetgearRouter: if self.model.startswith(model): self.method_version = 2 + if self.method_version == 2: + if not self._api.get_attached_devices_2(): + _LOGGER.error( + "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", + self.model, + ) + self.method_version = 1 + async def async_setup(self) -> None: """Set up a Netgear router.""" await self.hass.async_add_executor_job(self._setup) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index b6134216049..09313dab0dd 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,8 +3,8 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.5.3"], - "codeowners": ["@dieselrabbit"], + "requirements": ["screenlogicpy==0.5.4"], + "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "hostname": "pentair: *", diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index d4fce01fd78..b943013d4bc 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.2"], + "requirements": ["soco==0.25.3"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 69f9eddc6cd..59415d31c1e 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.13.0"], + "requirements": ["PySwitchbot==0.13.2"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index a09255d0392..48dbd8d8cac 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -113,6 +113,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): super().__init__(coordinator, idx, mac, name) self._attr_unique_id = idx self._device = device + self._attr_is_on = False async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" @@ -132,6 +133,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): ) if self._last_run_success: self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" @@ -143,6 +145,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): ) if self._last_run_success: self._attr_is_on = False + self.async_write_ha_state() @property def assumed_state(self) -> bool: diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 845d5e6d9c3..dab39f598f2 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -142,7 +142,7 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Turn on the fan.""" + """Turn on the fan. Auto-mode if no argument is given.""" if not self._device_control: return @@ -150,8 +150,8 @@ class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): await self._api(self._device_control.set_mode(_from_percentage(percentage))) return - if preset_mode: - await self.async_set_preset_mode(preset_mode) + preset_mode = preset_mode or ATTR_AUTO + await self.async_set_preset_mode(preset_mode) async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 35e6f5814f3..a56f64a8aad 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -148,8 +148,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._attr_temperature_unit = TEMP_CELSIUS if any( - "f" in device.status.get(dpcode, "").lower() + "f" in device.status[dpcode].lower() for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT) + if isinstance(device.status.get(dpcode), str) ): self._attr_temperature_unit = TEMP_FAHRENHEIT diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 0669dee86c4..c6640378de5 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -30,6 +30,28 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode from .util import remap_value +@dataclass +class ColorTypeData: + """Color Type Data.""" + + h_type: IntegerTypeData + s_type: IntegerTypeData + v_type: IntegerTypeData + + +DEFAULT_COLOR_TYPE_DATA = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=255, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=255, step=1), +) + +DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), +) + + @dataclass class TuyaLightEntityDescription(LightEntityDescription): """Describe an Tuya light entity.""" @@ -40,6 +62,7 @@ class TuyaLightEntityDescription(LightEntityDescription): color_data: DPCode | tuple[DPCode, ...] | None = None color_mode: DPCode | None = None color_temp: DPCode | tuple[DPCode, ...] | None = None + default_color_type: ColorTypeData = DEFAULT_COLOR_TYPE_DATA LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { @@ -63,6 +86,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, color_data=DPCode.COLOUR_DATA, + default_color_type=DEFAULT_COLOR_TYPE_DATA_V2, ), ), # Light @@ -242,28 +266,6 @@ LIGHTS["cz"] = LIGHTS["kg"] LIGHTS["pc"] = LIGHTS["kg"] -@dataclass -class ColorTypeData: - """Color Type Data.""" - - h_type: IntegerTypeData - s_type: IntegerTypeData - v_type: IntegerTypeData - - -DEFAULT_COLOR_TYPE_DATA = ColorTypeData( - h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), - s_type=IntegerTypeData(min=1, scale=0, max=255, step=1), - v_type=IntegerTypeData(min=1, scale=0, max=255, step=1), -) - -DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData( - h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), - s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), - v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), -) - - @dataclass class ColorData: """Color Data.""" @@ -443,7 +445,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ) else: # If no type is found, use a default one - self._color_data_type = DEFAULT_COLOR_TYPE_DATA + self._color_data_type = self.entity_description.default_color_type if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or ( self._brightness_type and self._brightness_type.max > 255 ): diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 832ec8a12e3..7991cbccbb4 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -2,7 +2,7 @@ "domain": "waze_travel_time", "name": "Waze Travel Time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", - "requirements": ["WazeRouteCalculator==0.13"], + "requirements": ["WazeRouteCalculator==0.14"], "codeowners": [], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 94b3412d44a..eacc39db560 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -7,6 +7,7 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_DEVICE, @@ -251,7 +252,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for feature, description in NUMBER_TYPES.items(): if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5: # Delete LED bightness entity created by mistake if it exists - entity_reg = hass.helpers.entity_registry.async_get() + entity_reg = er.async_get(hass) entity_id = entity_reg.async_get_entity_id( PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}" ) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 14d64f87eb7..f819a33f1d4 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -14,7 +14,14 @@ from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_DOMAIN, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -227,7 +234,22 @@ async def async_call_action_from_config( if action_type not in ACTION_TYPES: raise HomeAssistantError(f"Unhandled action type {action_type}") - service_data = {k: v for k, v in config.items() if v not in (None, "")} + # Don't include domain, subtype or any null/empty values in the service call + service_data = { + k: v + for k, v in config.items() + if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "") + } + + # Entity services (including refresh value which is a fake entity service) expects + # just an entity ID + if action_type in ( + SERVICE_REFRESH_VALUE, + SERVICE_SET_LOCK_USERCODE, + SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_RESET_METER, + ): + service_data.pop(ATTR_DEVICE_ID) await hass.services.async_call( DOMAIN, service, service_data, blocking=True, context=context ) @@ -283,7 +305,10 @@ async def async_get_action_capabilities( "extra_fields": vol.Schema( { vol.Required(ATTR_COMMAND_CLASS): vol.In( - {cc.value: cc.name for cc in CommandClass} + { + CommandClass(cc.id).value: cc.name + for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + } ), vol.Required(ATTR_PROPERTY): cv.string, vol.Optional(ATTR_PROPERTY_KEY): cv.string, diff --git a/homeassistant/const.py b/homeassistant/const.py index 45104f0d81b..16ee024024c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "8" +PATCH_VERSION: Final = "9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e90fdf9db12..1152007c417 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,8 +15,8 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 -hass-nabucasa==0.50.0 -home-assistant-frontend==20211229.0 +hass-nabucasa==0.51.0 +home-assistant-frontend==20211229.1 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 34002e25317..41c1908f511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.0 +# PySwitchbot==0.13.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -83,7 +83,7 @@ TwitterAPI==2.7.5 WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.13 +WazeRouteCalculator==0.14 # homeassistant.components.abode abodepy==1.2.0 @@ -177,7 +177,7 @@ aiogithubapi==21.11.0 aioguardian==2021.11.0 # homeassistant.components.harmony -aioharmony==0.2.8 +aioharmony==0.2.9 # homeassistant.components.homekit_controller aiohomekit==0.6.4 @@ -659,7 +659,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.32 +flux_led==0.27.45 # homeassistant.components.homekit fnvhash==0.1.0 @@ -787,7 +787,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.50.0 +hass-nabucasa==0.51.0 # homeassistant.components.splunk hass_splunk==0.1.1 @@ -820,7 +820,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211229.0 +home-assistant-frontend==20211229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -1397,7 +1397,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.2 +pychromecast==10.2.3 # homeassistant.components.pocketcasts pycketcasts==1.0.0 @@ -2113,7 +2113,7 @@ scapy==2.4.5 schiene==0.23 # homeassistant.components.screenlogic -screenlogicpy==0.5.3 +screenlogicpy==0.5.4 # homeassistant.components.scsgate scsgate==0.1.0 @@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.2 +soco==0.25.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 @@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.17 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccbcfd7d5e2..8beca5013ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -# PySwitchbot==0.13.0 +# PySwitchbot==0.13.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -45,7 +45,7 @@ RtmAPI==0.7.2 WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.13 +WazeRouteCalculator==0.14 # homeassistant.components.abode abodepy==1.2.0 @@ -121,7 +121,7 @@ aioflo==2021.11.0 aioguardian==2021.11.0 # homeassistant.components.harmony -aioharmony==0.2.8 +aioharmony==0.2.9 # homeassistant.components.homekit_controller aiohomekit==0.6.4 @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.32 +flux_led==0.27.45 # homeassistant.components.homekit fnvhash==0.1.0 @@ -494,7 +494,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.50.0 +hass-nabucasa==0.51.0 # homeassistant.components.tasmota hatasmota==0.3.1 @@ -515,7 +515,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211229.0 +home-assistant-frontend==20211229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -850,7 +850,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.2 +pychromecast==10.2.3 # homeassistant.components.climacell pyclimacell==0.18.2 @@ -1257,7 +1257,7 @@ samsungtvws==1.6.0 scapy==2.4.5 # homeassistant.components.screenlogic -screenlogicpy==0.5.3 +screenlogicpy==0.5.4 # homeassistant.components.emulated_kasa # homeassistant.components.sense @@ -1291,7 +1291,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.2 +soco==0.25.3 # homeassistant.components.solaredge solaredge==0.0.2 @@ -1464,7 +1464,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.17 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 13d8f18d0d9..7075eb84d72 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -2,8 +2,6 @@ import json import os import time - -# from unittest.mock import AsyncMock from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( @@ -207,6 +205,8 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, side_effect=api_call_side_effects["unlock_return_activities"] ) + api_instance.async_unlock_async = AsyncMock() + api_instance.async_lock_async = AsyncMock() api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) return await _mock_setup_august(hass, api_instance, pubnub) diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 26c824e5842..e2ff4a6771a 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,5 +1,6 @@ """The binary_sensor tests for the august platform.""" import datetime +import time from unittest.mock import Mock, patch from yalexs.pubnub_async import AugustPubNub @@ -26,6 +27,10 @@ from tests.components.august.mocks import ( ) +def _timetoken(): + return str(time.time_ns())[:-2] + + async def test_doorsense(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( @@ -85,6 +90,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF binary_sensor_k98gidt45gul_name_online = hass.states.get( "binary_sensor.k98gidt45gul_name_online" ) @@ -97,6 +106,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF async def test_create_doorbell_offline(hass): @@ -171,7 +184,7 @@ async def test_doorbell_update_via_pubnub(hass): pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "imagecapture", "data": { @@ -186,10 +199,46 @@ async def test_doorbell_update_via_pubnub(hass): await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON + + pubnub.message( + pubnub, + Mock( + channel=doorbell_one.pubsub_channel, + timetoken=_timetoken(), + message={ + "status": "doorbell_motion_detected", + "data": { + "event": "doorbell_motion_detected", + "image": { + "height": 640, + "width": 480, + "format": "jpg", + "created_at": "2021-03-16T02:36:26.886Z", + "bytes": 14061, + "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "etag": "09e839331c4ea59eef28081f2caa0e90", + }, + "doorbellName": "Front Door", + "callID": None, + "origin": "mars-api", + "mutableContent": True, + }, + }, + ), + ) + + await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_motion = hass.states.get( "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON + binary_sensor_k98gidt45gul_name_ding = hass.states.get( "binary_sensor.k98gidt45gul_name_ding" ) @@ -204,16 +253,16 @@ async def test_doorbell_update_via_pubnub(hass): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF pubnub.message( pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "buttonpush", }, @@ -274,7 +323,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "closed"}, ), ) @@ -289,11 +338,10 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Locking", "doorState": "open"}, ), ) - await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( "binary_sensor.online_with_doorsense_name_open" @@ -327,7 +375,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "open"}, ), ) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 9d1c34d917a..56f55138e36 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -154,6 +154,86 @@ async def test_one_lock_operation(hass): ) +async def test_one_lock_operation_pubnub_connected(hass): + """Test lock and unlock operations are async when pubnub is connected.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + assert lock_one.pubsub_channel == "pubsub" + + pubnub = AugustPubNub() + await _create_august_with_devices(hass, [lock_one], pubnub=pubnub) + pubnub.connected = True + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True + ) + await hass.async_block_till_done() + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 1) * 10000000, + message={ + "status": "kAugLockState_Unlocked", + }, + ), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + + assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 + assert ( + lock_online_with_doorsense_name.attributes.get("friendly_name") + == "online_with_doorsense Name" + ) + + assert await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True + ) + await hass.async_block_till_done() + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, + message={ + "status": "kAugLockState_Locked", + }, + ), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + # No activity means it will be unavailable until the activity feed has data + entity_registry = er.async_get(hass) + lock_operator_sensor = entity_registry.async_get( + "sensor.online_with_doorsense_name_operator" + ) + assert lock_operator_sensor + assert ( + hass.states.get("sensor.online_with_doorsense_name_operator").state + == STATE_UNKNOWN + ) + + async def test_lock_jammed(hass): """Test lock gets jammed on unlock.""" @@ -273,6 +353,7 @@ async def test_lock_update_via_pubnub(hass): config_entry = await _create_august_with_devices( hass, [lock_one], activities=activities, pubnub=pubnub ) + pubnub.connected = True lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 42a498528ce..ac8cd49802e 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -15,6 +15,7 @@ from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.core import State +from homeassistant.util.location import LocationInfo from . import mock_cloud, mock_cloud_prefs @@ -203,16 +204,60 @@ async def test_logout_view_unknown_error(hass, cloud_client): assert req.status == HTTPStatus.BAD_GATEWAY -async def test_register_view(mock_cognito, cloud_client): - """Test logging out.""" - req = await cloud_client.post( - "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} - ) +async def test_register_view_no_location(mock_cognito, cloud_client): + """Test register without location.""" + with patch( + "homeassistant.components.cloud.http_api.async_detect_location_info", + return_value=None, + ): + req = await cloud_client.post( + "/api/cloud/register", + json={"email": "hello@bla.com", "password": "falcon42"}, + ) assert req.status == HTTPStatus.OK assert len(mock_cognito.register.mock_calls) == 1 - result_email, result_pass = mock_cognito.register.mock_calls[0][1] + call = mock_cognito.register.mock_calls[0] + result_email, result_pass = call.args assert result_email == "hello@bla.com" assert result_pass == "falcon42" + assert call.kwargs["client_metadata"] is None + + +async def test_register_view_with_location(mock_cognito, cloud_client): + """Test register with location.""" + with patch( + "homeassistant.components.cloud.http_api.async_detect_location_info", + return_value=LocationInfo( + **{ + "country_code": "XX", + "zip_code": "12345", + "region_code": "GH", + "ip": "1.2.3.4", + "city": "Gotham", + "region_name": "Gotham", + "time_zone": "Earth/Gotham", + "currency": "XXX", + "latitude": "12.34567", + "longitude": "12.34567", + "use_metric": True, + } + ), + ): + req = await cloud_client.post( + "/api/cloud/register", + json={"email": "hello@bla.com", "password": "falcon42"}, + ) + assert req.status == HTTPStatus.OK + assert len(mock_cognito.register.mock_calls) == 1 + call = mock_cognito.register.mock_calls[0] + result_email, result_pass = call.args + assert result_email == "hello@bla.com" + assert result_pass == "falcon42" + assert call.kwargs["client_metadata"] == { + "NC_COUNTRY_CODE": "XX", + "NC_REGION_CODE": "GH", + "NC_ZIP_CODE": "12345", + } async def test_register_view_bad_data(mock_cognito, cloud_client): diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index f076d8e00ae..d190dec04b8 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -7,6 +7,8 @@ from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component +from .util import PATH_HOMEKIT, async_init_entry + from tests.common import MockConfigEntry @@ -1065,11 +1067,13 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_ip): +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_include_mode_basic_accessory( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf +): """Test config flow options in include mode with a single accessory.""" - config_entry = _mock_config_entry_with_options_populated() - config_entry.add_to_hass(hass) + await async_init_entry(hass, config_entry) hass.states.async_set("media_player.tv", "off") hass.states.async_set("media_player.sonos", "off") @@ -1101,7 +1105,48 @@ async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_i assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "include_exclude" - assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + assert _get_schema_default(result2["data_schema"].schema, "entities") is None + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": "media_player.tv"}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "accessory", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["media_player.tv"], + }, + } + + # Now we check again to make sure the single entity is still + # preselected + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": ["media_player"], + "mode": "accessory", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["media_player"], "mode": "accessory"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "include_exclude" + assert ( + _get_schema_default(result2["data_schema"].schema, "entities") + == "media_player.tv" + ) result3 = await hass.config_entries.options.async_configure( result2["flow_id"], diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 0b1d2cc8535..bd637572191 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -683,6 +683,11 @@ async def test_homekit_unpair(hass, device_reg, mock_async_zeroconf): state = homekit.driver.state state.add_paired_client("client1", "any", b"1") + state.add_paired_client("client2", "any", b"0") + state.add_paired_client("client3", "any", b"1") + state.add_paired_client("client4", "any", b"0") + state.add_paired_client("client5", "any", b"0") + formatted_mac = device_registry.format_mac(state.mac) hk_bridge_dev = device_reg.async_get_device( {}, {(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)} diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index b26dce8d936..bdb68d79ab2 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, ORBI_PORT +from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, PORT_80 from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -252,7 +252,7 @@ async def test_ssdp(hass, service): assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST - assert result["data"].get(CONF_PORT) == ORBI_PORT + assert result["data"].get(CONF_PORT) == PORT_80 assert result["data"].get(CONF_SSL) == SSL assert result["data"].get(CONF_USERNAME) == DEFAULT_USER assert result["data"][CONF_PASSWORD] == PASSWORD diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index 34df415301e..cd5a6bd4abe 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -57,7 +57,128 @@ }, { "nodeId": 13, "index": 2 } ], - "commandClasses": [], + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 65bc8e4bddb..0980b414a09 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -1,4 +1,6 @@ """The tests for Z-Wave JS device actions.""" +from unittest.mock import patch + import pytest import voluptuous_serialize from zwave_js_server.client import Client @@ -14,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations async def test_get_actions( @@ -87,8 +89,130 @@ async def test_get_actions_meter( assert len(filtered_actions) > 0 -async def test_action(hass: HomeAssistant) -> None: - """Test for turn_on and turn_off actions.""" +async def test_actions( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, +) -> None: + """Test actions.""" + node = climate_radio_thermostat_ct100_plus + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": "climate.z_wave_thermostat", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_ping", + }, + "action": { + "domain": DOMAIN, + "type": "ping", + "device_id": device.id, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_value", + }, + "action": { + "domain": DOMAIN, + "type": "set_value", + "device_id": device.id, + "command_class": 112, + "property": 1, + "value": 1, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_config_parameter", + }, + "action": { + "domain": DOMAIN, + "type": "set_config_parameter", + "device_id": device.id, + "parameter": 1, + "bitmask": None, + "subtype": "2-112-0-3 (Beeper)", + "value": 1, + }, + }, + ] + }, + ) + + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + with patch("zwave_js_server.model.node.Node.async_ping") as mock_call: + hass.bus.async_fire("test_event_ping") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 0 + + with patch("zwave_js_server.model.node.Node.async_set_value") as mock_call: + hass.bus.async_fire("test_event_set_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == "13-112-0-1" + assert args[1] == 1 + + with patch( + "homeassistant.components.zwave_js.services.async_set_config_parameter" + ) as mock_call: + hass.bus.async_fire("test_event_set_config_parameter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == 13 + assert args[1] == 1 + assert args[2] == 1 + + +async def test_lock_actions( + hass: HomeAssistant, + client: Client, + lock_schlage_be469: Node, + integration: ConfigEntry, +) -> None: + """Test actions for locks.""" + node = lock_schlage_be469 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + assert await async_setup_component( hass, automation.DOMAIN, @@ -102,7 +226,7 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "clear_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, }, @@ -115,97 +239,80 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "type": "set_lock_usercode", - "device_id": "fake", + "device_id": device.id, "entity_id": "lock.touchscreen_deadbolt", "code_slot": 1, "usercode": "1234", }, }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_refresh_value", - }, - "action": { - "domain": DOMAIN, - "type": "refresh_value", - "device_id": "fake", - "entity_id": "lock.touchscreen_deadbolt", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_ping", - }, - "action": { - "domain": DOMAIN, - "type": "ping", - "device_id": "fake", - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_value", - }, - "action": { - "domain": DOMAIN, - "type": "set_value", - "device_id": "fake", - "command_class": 112, - "property": "test", - "value": 1, - }, - }, - { - "trigger": { - "platform": "event", - "event_type": "test_event_set_config_parameter", - }, - "action": { - "domain": DOMAIN, - "type": "set_config_parameter", - "device_id": "fake", - "parameter": 3, - "bitmask": None, - "subtype": "2-112-0-3 (Beeper)", - "value": 255, - }, - }, ] }, ) - clear_lock_usercode = async_mock_service(hass, "zwave_js", "clear_lock_usercode") - hass.bus.async_fire("test_event_clear_lock_usercode") - await hass.async_block_till_done() - assert len(clear_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.clear_usercode") as mock_call: + hass.bus.async_fire("test_event_clear_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0].node_id == node.node_id + assert args[1] == 1 - set_lock_usercode = async_mock_service(hass, "zwave_js", "set_lock_usercode") - hass.bus.async_fire("test_event_set_lock_usercode") - await hass.async_block_till_done() - assert len(set_lock_usercode) == 1 + with patch("homeassistant.components.zwave_js.lock.set_usercode") as mock_call: + hass.bus.async_fire("test_event_set_lock_usercode") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == node.node_id + assert args[1] == 1 + assert args[2] == "1234" - refresh_value = async_mock_service(hass, "zwave_js", "refresh_value") - hass.bus.async_fire("test_event_refresh_value") - await hass.async_block_till_done() - assert len(refresh_value) == 1 - ping = async_mock_service(hass, "zwave_js", "ping") - hass.bus.async_fire("test_event_ping") - await hass.async_block_till_done() - assert len(ping) == 1 +async def test_reset_meter_action( + hass: HomeAssistant, + client: Client, + aeon_smart_switch_6: Node, + integration: ConfigEntry, +) -> None: + """Test reset_meter action.""" + node = aeon_smart_switch_6 + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device - set_value = async_mock_service(hass, "zwave_js", "set_value") - hass.bus.async_fire("test_event_set_value") - await hass.async_block_till_done() - assert len(set_value) == 1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_reset_meter", + }, + "action": { + "domain": DOMAIN, + "type": "reset_meter", + "device_id": device.id, + "entity_id": "sensor.smart_switch_6_electric_consumed_kwh", + }, + }, + ] + }, + ) - set_config_parameter = async_mock_service(hass, "zwave_js", "set_config_parameter") - hass.bus.async_fire("test_event_set_config_parameter") - await hass.async_block_till_done() - assert len(set_config_parameter) == 1 + with patch( + "zwave_js_server.model.endpoint.Endpoint.async_invoke_cc_api" + ) as mock_call: + hass.bus.async_fire("test_event_reset_meter") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 2 + assert args[0] == CommandClass.METER + assert args[1] == "reset" async def test_get_action_capabilities( @@ -261,7 +368,28 @@ async def test_get_action_capabilities( ) assert capabilities and "extra_fields" in capabilities - cc_options = [(cc.value, cc.name) for cc in CommandClass] + cc_options = [ + (133, "Association"), + (89, "Association Group Information"), + (128, "Battery"), + (129, "Clock"), + (112, "Configuration"), + (90, "Device Reset Locally"), + (122, "Firmware Update Meta Data"), + (135, "Indicator"), + (114, "Manufacturer Specific"), + (96, "Multi Channel"), + (142, "Multi Channel Association"), + (49, "Multilevel Sensor"), + (115, "Powerlevel"), + (68, "Thermostat Fan Mode"), + (69, "Thermostat Fan State"), + (64, "Thermostat Mode"), + (66, "Thermostat Operating State"), + (67, "Thermostat Setpoint"), + (134, "Version"), + (94, "Z-Wave Plus Info"), + ] assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer