Add more Foscam switches (#147409)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Foscam-wangzhengyu
2025-08-12 04:14:32 +08:00
committed by GitHub
parent 9e398ffc10
commit e394435d7c
11 changed files with 763 additions and 78 deletions

View File

@@ -30,7 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bo
verbose=False, verbose=False,
) )
coordinator = FoscamCoordinator(hass, entry, session) coordinator = FoscamCoordinator(hass, entry, session)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator 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: 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 @callback
def _update_unique_id( def _update_unique_id(

View File

@@ -11,3 +11,16 @@ CONF_STREAM = "stream"
SERVICE_PTZ = "ptz" SERVICE_PTZ = "ptz"
SERVICE_PTZ_PRESET = "ptz_preset" 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",
]

View File

@@ -1,8 +1,8 @@
"""The foscam coordinator object.""" """The foscam coordinator object."""
import asyncio import asyncio
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Any
from libpyfoscamcgi import FoscamCamera from libpyfoscamcgi import FoscamCamera
@@ -15,9 +15,35 @@ from .const import DOMAIN, LOGGER
type FoscamConfigEntry = ConfigEntry[FoscamCoordinator] 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.""" """Foscam coordinator."""
config_entry: FoscamConfigEntry
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
@@ -34,24 +60,82 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) )
self.session = session 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.""" """Fetch data from API endpoint."""
async with asyncio.timeout(10):
async with asyncio.timeout(30): return await self.hass.async_add_executor_job(self.gather_all_configs)
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

View File

@@ -13,19 +13,15 @@ from .coordinator import FoscamCoordinator
class FoscamEntity(CoordinatorEntity[FoscamCoordinator]): class FoscamEntity(CoordinatorEntity[FoscamCoordinator]):
"""Base entity for Foscam camera.""" """Base entity for Foscam camera."""
def __init__( def __init__(self, coordinator: FoscamCoordinator, config_entry_id: str) -> None:
self,
coordinator: FoscamCoordinator,
entry_id: str,
) -> None:
"""Initialize the base Foscam entity.""" """Initialize the base Foscam entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry_id)}, identifiers={(DOMAIN, config_entry_id)},
manufacturer="Foscam", 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_MODEL] = dev_info["productName"]
self._attr_device_info[ATTR_SW_VERSION] = dev_info["firmwareVer"] self._attr_device_info[ATTR_SW_VERSION] = dev_info["firmwareVer"]
self._attr_device_info[ATTR_HW_VERSION] = dev_info["hardwareVer"] self._attr_device_info[ATTR_HW_VERSION] = dev_info["hardwareVer"]

View File

@@ -6,5 +6,39 @@
"ptz_preset": { "ptz_preset": {
"service": "mdi:target-variant" "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"
}
}
} }
} }

View File

@@ -27,8 +27,35 @@
}, },
"entity": { "entity": {
"switch": { "switch": {
"flip_switch": {
"name": "Flip"
},
"mirror_switch": {
"name": "Mirror"
},
"ir_switch": {
"name": "Infrared mode"
},
"sleep_switch": { "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"
} }
} }
}, },

View File

@@ -2,18 +2,117 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any from typing import Any
from homeassistant.components.switch import SwitchEntity from libpyfoscamcgi import FoscamCamera
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import LOGGER
from .coordinator import FoscamConfigEntry, FoscamCoordinator from .coordinator import FoscamConfigEntry, FoscamCoordinator
from .entity import FoscamEntity 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: FoscamConfigEntry, config_entry: FoscamConfigEntry,
@@ -22,63 +121,61 @@ async def async_setup_entry(
"""Set up foscam switch from a config entry.""" """Set up foscam switch from a config entry."""
coordinator = config_entry.runtime_data coordinator = config_entry.runtime_data
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
if coordinator.data["is_asleep"]["supported"]: entities = []
async_add_entities([FoscamSleepSwitch(coordinator, config_entry)])
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): class FoscamGenericSwitch(FoscamEntity, SwitchEntity):
"""An implementation for Sleep Switch.""" """A generic switch class for Foscam entities."""
_attr_has_entity_name = True
entity_description: FoscamSwitchEntityDescription
def __init__( def __init__(
self, self,
coordinator: FoscamCoordinator, coordinator: FoscamCoordinator,
config_entry: FoscamConfigEntry, description: FoscamSwitchEntityDescription,
) -> None: ) -> None:
"""Initialize a Foscam Sleep Switch.""" """Initialize the generic switch."""
super().__init__(coordinator, config_entry.entry_id) entry_id = coordinator.config_entry.entry_id
super().__init__(coordinator, entry_id)
self._attr_unique_id = f"{config_entry.entry_id}_sleep_switch" self.entity_description = description
self._attr_translation_key = "sleep_switch" self._attr_unique_id = f"{entry_id}_{description.key}"
self._attr_has_entity_name = True
self.is_asleep = self.coordinator.data["is_asleep"]["status"]
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if camera is asleep.""" """Return the state of the switch."""
return self.is_asleep return self.entity_description.native_value_fn(self.coordinator.data)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Wake camera.""" """Turn off the entity."""
LOGGER.debug("Wake camera") self.hass.async_add_executor_job(
self.entity_description.turn_off_fn, self.coordinator.session
ret, _ = await self.hass.async_add_executor_job(
self.coordinator.session.wake_up
) )
if ret != 0:
raise HomeAssistantError(f"Error waking up: {ret}")
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""But camera is sleep.""" """Turn on the entity."""
LOGGER.debug("Sleep camera") self.hass.async_add_executor_job(
self.entity_description.turn_on_fn, self.coordinator.session
ret, _ = await self.hass.async_add_executor_job(self.coordinator.session.sleep) )
if ret != 0:
raise HomeAssistantError(f"Error sleeping: {ret}")
await self.coordinator.async_request_refresh() 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()

View File

@@ -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_dev_info.return_value = (dev_info_rc, dev_info_data)
mock_foscam_camera.get_port_info.return_value = (dev_info_rc, {}) mock_foscam_camera.get_port_info.return_value = (dev_info_rc, {})
mock_foscam_camera.is_asleep.return_value = (0, True) 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 return mock_foscam_camera

View File

@@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.mock_title_flip',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_flip',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.mock_title_infrared_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_infrared_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_entities[switch.mock_title_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.mock_title_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.mock_title_mirror-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.mock_title_mirror',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_mirror',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.mock_title_siren_alarm-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_siren_alarm',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_entities[switch.mock_title_sleep_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_sleep_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.mock_title_volume_muted-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_volume_muted',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.mock_title_white_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.mock_title_white_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -96,7 +96,7 @@ async def test_unique_id_migration_not_needed(
assert entity_before.unique_id == f"{ENTRY_ID}_sleep_switch" assert entity_before.unique_id == f"{ENTRY_ID}_sleep_switch"
with ( with (
# Mock a valid camera instance" # Mock a valid camera instance
patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera, patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera,
patch( patch(
"homeassistant.components.foscam.async_migrate_entry", "homeassistant.components.foscam.async_migrate_entry",

View File

@@ -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)