diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 222a7e44a45..099123ccd9b 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -30,7 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bo verbose=False, ) coordinator = FoscamCoordinator(hass, entry, session) - await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -89,7 +88,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> async def async_migrate_entities(hass: HomeAssistant, entry: FoscamConfigEntry) -> None: - """Migrate old entry.""" + """Migrate old entries to support config_entry_id-based unique IDs.""" @callback def _update_unique_id( diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index 38088cf3f6f..33c1b31aeec 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -11,3 +11,16 @@ CONF_STREAM = "stream" SERVICE_PTZ = "ptz" SERVICE_PTZ_PRESET = "ptz_preset" + +SUPPORTED_SWITCHES = [ + "flip_switch", + "mirror_switch", + "ir_switch", + "sleep_switch", + "white_light_switch", + "siren_alarm_switch", + "turn_off_volume_switch", + "light_status_switch", + "hdr_switch", + "wdr_switch", +] diff --git a/homeassistant/components/foscam/coordinator.py b/homeassistant/components/foscam/coordinator.py index 72bf60cffe0..50ddd76ddb3 100644 --- a/homeassistant/components/foscam/coordinator.py +++ b/homeassistant/components/foscam/coordinator.py @@ -1,8 +1,8 @@ """The foscam coordinator object.""" import asyncio +from dataclasses import dataclass from datetime import timedelta -from typing import Any from libpyfoscamcgi import FoscamCamera @@ -15,9 +15,35 @@ from .const import DOMAIN, LOGGER type FoscamConfigEntry = ConfigEntry[FoscamCoordinator] -class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): +@dataclass +class FoscamDeviceInfo: + """A data class representing the current state and configuration of a Foscam camera device.""" + + dev_info: dict + product_info: dict + + is_open_ir: bool + is_flip: bool + is_mirror: bool + + is_asleep: dict + is_open_white_light: bool + is_siren_alarm: bool + + volume: int + speak_volume: int + is_turn_off_volume: bool + is_turn_off_light: bool + + is_open_wdr: bool | None = None + is_open_hdr: bool | None = None + + +class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): """Foscam coordinator.""" + config_entry: FoscamConfigEntry + def __init__( self, hass: HomeAssistant, @@ -34,24 +60,82 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) self.session = session - async def _async_update_data(self) -> dict[str, Any]: + def gather_all_configs(self) -> FoscamDeviceInfo: + """Get all Foscam configurations.""" + ret_dev_info, dev_info = self.session.get_dev_info() + dev_info = dev_info if ret_dev_info == 0 else {} + + ret_product_info, product_info = self.session.get_product_all_info() + product_info = product_info if ret_product_info == 0 else {} + + ret_ir, infra_led_config = self.session.get_infra_led_config() + is_open_ir = infra_led_config["mode"] == "1" if ret_ir == 0 else False + + ret_mf, mirror_flip_setting = self.session.get_mirror_and_flip_setting() + is_flip = mirror_flip_setting["isFlip"] == "1" if ret_mf == 0 else False + is_mirror = mirror_flip_setting["isMirror"] == "1" if ret_mf == 0 else False + + ret_sleep, sleep_setting = self.session.is_asleep() + is_asleep = {"supported": ret_sleep == 0, "status": bool(int(sleep_setting))} + + ret_wl, is_open_white_light = self.session.getWhiteLightBrightness() + is_open_white_light_val = ( + is_open_white_light["enable"] == "1" if ret_wl == 0 else False + ) + + ret_sc, is_siren_alarm = self.session.getSirenConfig() + is_siren_alarm_val = ( + is_siren_alarm["sirenEnable"] == "1" if ret_sc == 0 else False + ) + + ret_vol, volume = self.session.getAudioVolume() + volume_val = int(volume["volume"]) if ret_vol == 0 else 0 + + ret_sv, speak_volume = self.session.getSpeakVolume() + speak_volume_val = int(speak_volume["SpeakVolume"]) if ret_sv == 0 else 0 + + ret_ves, is_turn_off_volume = self.session.getVoiceEnableState() + is_turn_off_volume_val = not ( + ret_ves == 0 and is_turn_off_volume["isEnable"] == "1" + ) + + ret_les, is_turn_off_light = self.session.getLedEnableState() + is_turn_off_light_val = not ( + ret_les == 0 and is_turn_off_light["isEnable"] == "0" + ) + + is_open_wdr = None + is_open_hdr = None + reserve3 = product_info.get("reserve3") + reserve3_int = int(reserve3) if reserve3 is not None else 0 + + if (reserve3_int & (1 << 8)) != 0: + ret_wdr, is_open_wdr_data = self.session.getWdrMode() + mode = is_open_wdr_data["mode"] if ret_wdr == 0 and is_open_wdr_data else 0 + is_open_wdr = bool(int(mode)) + else: + ret_hdr, is_open_hdr_data = self.session.getHdrMode() + mode = is_open_hdr_data["mode"] if ret_hdr == 0 and is_open_hdr_data else 0 + is_open_hdr = bool(int(mode)) + + return FoscamDeviceInfo( + dev_info=dev_info, + product_info=product_info, + is_open_ir=is_open_ir, + is_flip=is_flip, + is_mirror=is_mirror, + is_asleep=is_asleep, + is_open_white_light=is_open_white_light_val, + is_siren_alarm=is_siren_alarm_val, + volume=volume_val, + speak_volume=speak_volume_val, + is_turn_off_volume=is_turn_off_volume_val, + is_turn_off_light=is_turn_off_light_val, + is_open_wdr=is_open_wdr, + is_open_hdr=is_open_hdr, + ) + + async def _async_update_data(self) -> FoscamDeviceInfo: """Fetch data from API endpoint.""" - - async with asyncio.timeout(30): - data = {} - ret, dev_info = await self.hass.async_add_executor_job( - self.session.get_dev_info - ) - if ret == 0: - data["dev_info"] = dev_info - - all_info = await self.hass.async_add_executor_job( - self.session.get_product_all_info - ) - data["product_info"] = all_info[1] - - ret, is_asleep = await self.hass.async_add_executor_job( - self.session.is_asleep - ) - data["is_asleep"] = {"supported": ret == 0, "status": is_asleep} - return data + async with asyncio.timeout(10): + return await self.hass.async_add_executor_job(self.gather_all_configs) diff --git a/homeassistant/components/foscam/entity.py b/homeassistant/components/foscam/entity.py index e9d1bbbe176..7bc983cbfaa 100644 --- a/homeassistant/components/foscam/entity.py +++ b/homeassistant/components/foscam/entity.py @@ -13,19 +13,15 @@ from .coordinator import FoscamCoordinator class FoscamEntity(CoordinatorEntity[FoscamCoordinator]): """Base entity for Foscam camera.""" - def __init__( - self, - coordinator: FoscamCoordinator, - entry_id: str, - ) -> None: + def __init__(self, coordinator: FoscamCoordinator, config_entry_id: str) -> None: """Initialize the base Foscam entity.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry_id)}, + identifiers={(DOMAIN, config_entry_id)}, manufacturer="Foscam", ) - if dev_info := coordinator.data.get("dev_info"): + if dev_info := coordinator.data.dev_info: self._attr_device_info[ATTR_MODEL] = dev_info["productName"] self._attr_device_info[ATTR_SW_VERSION] = dev_info["firmwareVer"] self._attr_device_info[ATTR_HW_VERSION] = dev_info["hardwareVer"] diff --git a/homeassistant/components/foscam/icons.json b/homeassistant/components/foscam/icons.json index 437575024d1..4b0b0c17c32 100644 --- a/homeassistant/components/foscam/icons.json +++ b/homeassistant/components/foscam/icons.json @@ -6,5 +6,39 @@ "ptz_preset": { "service": "mdi:target-variant" } + }, + "entity": { + "switch": { + "flip_switch": { + "default": "mdi:flip-vertical" + }, + "mirror_switch": { + "default": "mdi:mirror" + }, + "ir_switch": { + "default": "mdi:theme-light-dark" + }, + "sleep_switch": { + "default": "mdi:sleep" + }, + "white_light_switch": { + "default": "mdi:light-flood-down" + }, + "siren_alarm_switch": { + "default": "mdi:alarm-note" + }, + "turn_off_volume_switch": { + "default": "mdi:volume-off" + }, + "turn_off_light_switch": { + "default": "mdi:lightbulb-fluorescent-tube" + }, + "hdr_switch": { + "default": "mdi:hdr" + }, + "wdr_switch": { + "default": "mdi:alpha-w-box" + } + } } } diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 03351e3238f..29a74fdd2b4 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -27,8 +27,35 @@ }, "entity": { "switch": { + "flip_switch": { + "name": "Flip" + }, + "mirror_switch": { + "name": "Mirror" + }, + "ir_switch": { + "name": "Infrared mode" + }, "sleep_switch": { - "name": "Sleep" + "name": "Sleep mode" + }, + "white_light_switch": { + "name": "White light" + }, + "siren_alarm_switch": { + "name": "Siren alarm" + }, + "turn_off_volume_switch": { + "name": "Volume muted" + }, + "turn_off_light_switch": { + "name": "Light" + }, + "hdr_switch": { + "name": "HDR" + }, + "wdr_switch": { + "name": "WDR" } } }, diff --git a/homeassistant/components/foscam/switch.py b/homeassistant/components/foscam/switch.py index 24b05b5aeaa..91118a27277 100644 --- a/homeassistant/components/foscam/switch.py +++ b/homeassistant/components/foscam/switch.py @@ -2,18 +2,117 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from libpyfoscamcgi import FoscamCamera + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import LOGGER from .coordinator import FoscamConfigEntry, FoscamCoordinator from .entity import FoscamEntity +def handle_ir_turn_on(session: FoscamCamera) -> None: + """Turn on IR LED: sets IR mode to auto (if supported), then turns off the IR LED.""" + + session.set_infra_led_config(1) + session.open_infra_led() + + +def handle_ir_turn_off(session: FoscamCamera) -> None: + """Turn off IR LED: sets IR mode to manual (if supported), then turns open the IR LED.""" + + session.set_infra_led_config(0) + session.close_infra_led() + + +@dataclass(frozen=True, kw_only=True) +class FoscamSwitchEntityDescription(SwitchEntityDescription): + """A custom entity description that supports a turn_off function.""" + + native_value_fn: Callable[..., bool] + turn_off_fn: Callable[[FoscamCamera], None] + turn_on_fn: Callable[[FoscamCamera], None] + + +SWITCH_DESCRIPTIONS: list[FoscamSwitchEntityDescription] = [ + FoscamSwitchEntityDescription( + key="is_flip", + translation_key="flip_switch", + native_value_fn=lambda data: data.is_flip, + turn_off_fn=lambda session: session.flip_video(0), + turn_on_fn=lambda session: session.flip_video(1), + ), + FoscamSwitchEntityDescription( + key="is_mirror", + translation_key="mirror_switch", + native_value_fn=lambda data: data.is_mirror, + turn_off_fn=lambda session: session.mirror_video(0), + turn_on_fn=lambda session: session.mirror_video(1), + ), + FoscamSwitchEntityDescription( + key="is_open_ir", + translation_key="ir_switch", + native_value_fn=lambda data: data.is_open_ir, + turn_off_fn=handle_ir_turn_off, + turn_on_fn=handle_ir_turn_on, + ), + FoscamSwitchEntityDescription( + key="sleep_switch", + translation_key="sleep_switch", + native_value_fn=lambda data: data.is_asleep["status"], + turn_off_fn=lambda session: session.wake_up(), + turn_on_fn=lambda session: session.sleep(), + ), + FoscamSwitchEntityDescription( + key="is_open_white_light", + translation_key="white_light_switch", + native_value_fn=lambda data: data.is_open_white_light, + turn_off_fn=lambda session: session.closeWhiteLight(), + turn_on_fn=lambda session: session.openWhiteLight(), + ), + FoscamSwitchEntityDescription( + key="is_siren_alarm", + translation_key="siren_alarm_switch", + native_value_fn=lambda data: data.is_siren_alarm, + turn_off_fn=lambda session: session.setSirenConfig(0, 100, 0), + turn_on_fn=lambda session: session.setSirenConfig(1, 100, 0), + ), + FoscamSwitchEntityDescription( + key="is_turn_off_volume", + translation_key="turn_off_volume_switch", + native_value_fn=lambda data: data.is_turn_off_volume, + turn_off_fn=lambda session: session.setVoiceEnableState(1), + turn_on_fn=lambda session: session.setVoiceEnableState(0), + ), + FoscamSwitchEntityDescription( + key="is_turn_off_light", + translation_key="turn_off_light_switch", + native_value_fn=lambda data: data.is_turn_off_light, + turn_off_fn=lambda session: session.setLedEnableState(0), + turn_on_fn=lambda session: session.setLedEnableState(1), + ), + FoscamSwitchEntityDescription( + key="is_open_hdr", + translation_key="hdr_switch", + native_value_fn=lambda data: data.is_open_hdr, + turn_off_fn=lambda session: session.setHdrMode(0), + turn_on_fn=lambda session: session.setHdrMode(1), + ), + FoscamSwitchEntityDescription( + key="is_open_wdr", + translation_key="wdr_switch", + native_value_fn=lambda data: data.is_open_wdr, + turn_off_fn=lambda session: session.setWdrMode(0), + turn_on_fn=lambda session: session.setWdrMode(1), + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: FoscamConfigEntry, @@ -22,63 +121,61 @@ async def async_setup_entry( """Set up foscam switch from a config entry.""" coordinator = config_entry.runtime_data - await coordinator.async_config_entry_first_refresh() - if coordinator.data["is_asleep"]["supported"]: - async_add_entities([FoscamSleepSwitch(coordinator, config_entry)]) + entities = [] + + product_info = coordinator.data.product_info + reserve3 = product_info.get("reserve3", "0") + + for description in SWITCH_DESCRIPTIONS: + if description.key == "is_asleep": + if not coordinator.data.is_asleep["supported"]: + continue + elif description.key == "is_open_hdr": + if ((1 << 8) & int(reserve3)) != 0 or ((1 << 7) & int(reserve3)) == 0: + continue + elif description.key == "is_open_wdr": + if ((1 << 8) & int(reserve3)) == 0: + continue + + entities.append(FoscamGenericSwitch(coordinator, description)) + async_add_entities(entities) -class FoscamSleepSwitch(FoscamEntity, SwitchEntity): - """An implementation for Sleep Switch.""" +class FoscamGenericSwitch(FoscamEntity, SwitchEntity): + """A generic switch class for Foscam entities.""" + + _attr_has_entity_name = True + entity_description: FoscamSwitchEntityDescription def __init__( self, coordinator: FoscamCoordinator, - config_entry: FoscamConfigEntry, + description: FoscamSwitchEntityDescription, ) -> None: - """Initialize a Foscam Sleep Switch.""" - super().__init__(coordinator, config_entry.entry_id) + """Initialize the generic switch.""" + entry_id = coordinator.config_entry.entry_id + super().__init__(coordinator, entry_id) - self._attr_unique_id = f"{config_entry.entry_id}_sleep_switch" - self._attr_translation_key = "sleep_switch" - self._attr_has_entity_name = True - - self.is_asleep = self.coordinator.data["is_asleep"]["status"] + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{description.key}" @property - def is_on(self): - """Return true if camera is asleep.""" - return self.is_asleep + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.entity_description.native_value_fn(self.coordinator.data) async def async_turn_off(self, **kwargs: Any) -> None: - """Wake camera.""" - LOGGER.debug("Wake camera") - - ret, _ = await self.hass.async_add_executor_job( - self.coordinator.session.wake_up + """Turn off the entity.""" + self.hass.async_add_executor_job( + self.entity_description.turn_off_fn, self.coordinator.session ) - - if ret != 0: - raise HomeAssistantError(f"Error waking up: {ret}") - await self.coordinator.async_request_refresh() async def async_turn_on(self, **kwargs: Any) -> None: - """But camera is sleep.""" - LOGGER.debug("Sleep camera") - - ret, _ = await self.hass.async_add_executor_job(self.coordinator.session.sleep) - - if ret != 0: - raise HomeAssistantError(f"Error sleeping: {ret}") - + """Turn on the entity.""" + self.hass.async_add_executor_job( + self.entity_description.turn_on_fn, self.coordinator.session + ) await self.coordinator.async_request_refresh() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - - self.is_asleep = self.coordinator.data["is_asleep"]["status"] - - self.async_write_ha_state() diff --git a/tests/components/foscam/conftest.py b/tests/components/foscam/conftest.py index f8b4093574f..43616693303 100644 --- a/tests/components/foscam/conftest.py +++ b/tests/components/foscam/conftest.py @@ -60,6 +60,21 @@ def setup_mock_foscam_camera(mock_foscam_camera): mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data) mock_foscam_camera.get_port_info.return_value = (dev_info_rc, {}) mock_foscam_camera.is_asleep.return_value = (0, True) + mock_foscam_camera.get_infra_led_config.return_value = (0, {"mode": "1"}) + mock_foscam_camera.get_mirror_and_flip_setting.return_value = ( + 0, + {"isFlip": "0", "isMirror": "0"}, + ) + mock_foscam_camera.is_asleep.return_value = (0, "0") + mock_foscam_camera.getWhiteLightBrightness.return_value = (0, {"enable": "1"}) + mock_foscam_camera.getSirenConfig.return_value = (0, {"sirenEnable": "1"}) + mock_foscam_camera.getAudioVolume.return_value = (0, {"volume": "100"}) + mock_foscam_camera.getSpeakVolume.return_value = (0, {"SpeakVolume": "100"}) + mock_foscam_camera.getVoiceEnableState.return_value = (0, {"isEnable": "1"}) + mock_foscam_camera.getLedEnableState.return_value = (0, {"isEnable": "0"}) + mock_foscam_camera.getWdrMode.return_value = (0, {"mode": "0"}) + mock_foscam_camera.getHdrMode.return_value = (0, {"mode": "0"}) + mock_foscam_camera.get_motion_detect_config.return_value = (0, 1) return mock_foscam_camera diff --git a/tests/components/foscam/snapshots/test_switch.ambr b/tests/components/foscam/snapshots/test_switch.ambr new file mode 100644 index 00000000000..f48df6b65e6 --- /dev/null +++ b/tests/components/foscam/snapshots/test_switch.ambr @@ -0,0 +1,385 @@ +# serializer version: 1 +# name: test_entities[switch.mock_title_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip_switch', + 'unique_id': '123ABC_is_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Flip', + }), + 'context': , + 'entity_id': 'switch.mock_title_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_infrared_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_infrared_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Infrared mode', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ir_switch', + 'unique_id': '123ABC_is_open_ir', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_infrared_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Infrared mode', + }), + 'context': , + 'entity_id': 'switch.mock_title_infrared_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[switch.mock_title_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_off_light_switch', + 'unique_id': '123ABC_is_turn_off_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Light', + }), + 'context': , + 'entity_id': 'switch.mock_title_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_mirror-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_mirror', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mirror', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mirror_switch', + 'unique_id': '123ABC_is_mirror', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_mirror-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Mirror', + }), + 'context': , + 'entity_id': 'switch.mock_title_mirror', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_siren_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_siren_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Siren alarm', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'siren_alarm_switch', + 'unique_id': '123ABC_is_siren_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_siren_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Siren alarm', + }), + 'context': , + 'entity_id': 'switch.mock_title_siren_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[switch.mock_title_sleep_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_sleep_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sleep mode', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sleep_switch', + 'unique_id': '123ABC_sleep_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_sleep_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Sleep mode', + }), + 'context': , + 'entity_id': 'switch.mock_title_sleep_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_volume_muted-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_volume_muted', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume muted', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_off_volume_switch', + 'unique_id': '123ABC_is_turn_off_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_volume_muted-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Volume muted', + }), + 'context': , + 'entity_id': 'switch.mock_title_volume_muted', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_white_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'White light', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'white_light_switch', + 'unique_id': '123ABC_is_open_white_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title White light', + }), + 'context': , + 'entity_id': 'switch.mock_title_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/foscam/test_init.py b/tests/components/foscam/test_init.py index a7b6a8c8f0b..7c7b1b8aee8 100644 --- a/tests/components/foscam/test_init.py +++ b/tests/components/foscam/test_init.py @@ -96,7 +96,7 @@ async def test_unique_id_migration_not_needed( assert entity_before.unique_id == f"{ENTRY_ID}_sleep_switch" with ( - # Mock a valid camera instance" + # Mock a valid camera instance patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera, patch( "homeassistant.components.foscam.async_migrate_entry", diff --git a/tests/components/foscam/test_switch.py b/tests/components/foscam/test_switch.py new file mode 100644 index 00000000000..bd9eb380fbd --- /dev/null +++ b/tests/components/foscam/test_switch.py @@ -0,0 +1,35 @@ +"""Test for the switch platform entity of the foscam component.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.foscam.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_mock_foscam_camera +from .const import ENTRY_ID, VALID_CONFIG + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that coordinator returns the data we expect after the first refresh.""" + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID) + entry.add_to_hass(hass) + + with ( + # Mock a valid camera instance" + patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera, + patch("homeassistant.components.foscam.PLATFORMS", [Platform.SWITCH]), + ): + setup_mock_foscam_camera(mock_foscam_camera) + assert await hass.config_entries.async_setup(entry.entry_id) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)