diff --git a/homeassistant/components/foscam/coordinator.py b/homeassistant/components/foscam/coordinator.py index 80b6ec96e83..c4380ffb097 100644 --- a/homeassistant/components/foscam/coordinator.py +++ b/homeassistant/components/foscam/coordinator.py @@ -35,9 +35,16 @@ class FoscamDeviceInfo: is_turn_off_volume: bool is_turn_off_light: bool supports_speak_volume_adjustment: bool + supports_pet_adjustment: bool + supports_car_adjustment: bool + supports_wdr_adjustment: bool + supports_hdr_adjustment: bool is_open_wdr: bool | None = None is_open_hdr: bool | None = None + is_pet_detection_on: bool | None = None + is_car_detection_on: bool | None = None + is_human_detection_on: bool | None = None class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): @@ -107,14 +114,15 @@ class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): is_open_wdr = None is_open_hdr = None - reserve3 = product_info.get("reserve3") + reserve3 = product_info.get("reserve4") reserve3_int = int(reserve3) if reserve3 is not None else 0 - - if (reserve3_int & (1 << 8)) != 0: + supports_wdr_adjustment_val = bool(int(reserve3_int & 256)) + supports_hdr_adjustment_val = bool(int(reserve3_int & 128)) + if supports_wdr_adjustment_val: 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: + elif supports_hdr_adjustment_val: 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)) @@ -126,6 +134,34 @@ class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): if ret_sw == 0 else False ) + pet_adjustment_val = ( + bool(int(software_capabilities.get("swCapabilities2")) & 512) + if ret_sw == 0 + else False + ) + car_adjustment_val = ( + bool(int(software_capabilities.get("swCapabilities2")) & 256) + if ret_sw == 0 + else False + ) + ret_md, mothion_config_val = self.session.get_motion_detect_config() + if pet_adjustment_val: + is_pet_detection_on_val = ( + mothion_config_val["petEnable"] == "1" if ret_md == 0 else False + ) + else: + is_pet_detection_on_val = False + + if car_adjustment_val: + is_car_detection_on_val = ( + mothion_config_val["carEnable"] == "1" if ret_md == 0 else False + ) + else: + is_car_detection_on_val = False + + is_human_detection_on_val = ( + mothion_config_val["humanEnable"] == "1" if ret_md == 0 else False + ) return FoscamDeviceInfo( dev_info=dev_info, @@ -141,8 +177,15 @@ class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): is_turn_off_volume=is_turn_off_volume_val, is_turn_off_light=is_turn_off_light_val, supports_speak_volume_adjustment=supports_speak_volume_adjustment_val, + supports_pet_adjustment=pet_adjustment_val, + supports_car_adjustment=car_adjustment_val, + supports_hdr_adjustment=supports_hdr_adjustment_val, + supports_wdr_adjustment=supports_wdr_adjustment_val, is_open_wdr=is_open_wdr, is_open_hdr=is_open_hdr, + is_pet_detection_on=is_pet_detection_on_val, + is_car_detection_on=is_car_detection_on_val, + is_human_detection_on=is_human_detection_on_val, ) async def _async_update_data(self) -> FoscamDeviceInfo: diff --git a/homeassistant/components/foscam/icons.json b/homeassistant/components/foscam/icons.json index 7dbd874b2f6..de9e13faa00 100644 --- a/homeassistant/components/foscam/icons.json +++ b/homeassistant/components/foscam/icons.json @@ -38,6 +38,15 @@ }, "wdr_switch": { "default": "mdi:alpha-w-box" + }, + "pet_detection": { + "default": "mdi:paw" + }, + "car_detection": { + "default": "mdi:car-hatchback" + }, + "human_detection": { + "default": "mdi:human" } }, "number": { diff --git a/homeassistant/components/foscam/number.py b/homeassistant/components/foscam/number.py index e828955870d..a693685c67e 100644 --- a/homeassistant/components/foscam/number.py +++ b/homeassistant/components/foscam/number.py @@ -22,7 +22,7 @@ class FoscamNumberEntityDescription(NumberEntityDescription): native_value_fn: Callable[[FoscamCoordinator], int] set_value_fn: Callable[[FoscamCamera, float], Any] - exists_fn: Callable[[FoscamCoordinator], bool] + exists_fn: Callable[[FoscamCoordinator], bool] = lambda _: True NUMBER_DESCRIPTIONS: list[FoscamNumberEntityDescription] = [ @@ -34,7 +34,6 @@ NUMBER_DESCRIPTIONS: list[FoscamNumberEntityDescription] = [ native_step=1, native_value_fn=lambda coordinator: coordinator.data.device_volume, set_value_fn=lambda session, value: session.setAudioVolume(value), - exists_fn=lambda _: True, ), FoscamNumberEntityDescription( key="speak_volume", diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 86a5ba59c0a..e42a74a2683 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -61,6 +61,15 @@ }, "wdr_switch": { "name": "WDR" + }, + "pet_detection": { + "name": "Pet detection" + }, + "car_detection": { + "name": "Car detection" + }, + "human_detection": { + "name": "Human detection" } }, "number": { diff --git a/homeassistant/components/foscam/switch.py b/homeassistant/components/foscam/switch.py index 8407da8edd3..dbfdad2e0e0 100644 --- a/homeassistant/components/foscam/switch.py +++ b/homeassistant/components/foscam/switch.py @@ -30,6 +30,14 @@ def handle_ir_turn_off(session: FoscamCamera) -> None: session.close_infra_led() +def set_motion_detection(session: FoscamCamera, field: str, enabled: bool) -> None: + """Turns on pet detection.""" + ret, config = session.get_motion_detect_config() + if not ret: + config[field] = int(enabled) + session.set_motion_detect_config(config) + + @dataclass(frozen=True, kw_only=True) class FoscamSwitchEntityDescription(SwitchEntityDescription): """A custom entity description that supports a turn_off function.""" @@ -37,6 +45,7 @@ class FoscamSwitchEntityDescription(SwitchEntityDescription): native_value_fn: Callable[..., bool] turn_off_fn: Callable[[FoscamCamera], None] turn_on_fn: Callable[[FoscamCamera], None] + exists_fn: Callable[[FoscamCoordinator], bool] = lambda _: True SWITCH_DESCRIPTIONS: list[FoscamSwitchEntityDescription] = [ @@ -102,6 +111,7 @@ SWITCH_DESCRIPTIONS: list[FoscamSwitchEntityDescription] = [ 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), + exists_fn=lambda coordinator: coordinator.data.supports_hdr_adjustment, ), FoscamSwitchEntityDescription( key="is_open_wdr", @@ -109,6 +119,30 @@ SWITCH_DESCRIPTIONS: list[FoscamSwitchEntityDescription] = [ 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), + exists_fn=lambda coordinator: coordinator.data.supports_wdr_adjustment, + ), + FoscamSwitchEntityDescription( + key="pet_detection", + translation_key="pet_detection", + native_value_fn=lambda data: data.is_pet_detection_on, + turn_off_fn=lambda session: set_motion_detection(session, "petEnable", False), + turn_on_fn=lambda session: set_motion_detection(session, "petEnable", True), + exists_fn=lambda coordinator: coordinator.data.supports_pet_adjustment, + ), + FoscamSwitchEntityDescription( + key="car_detection", + translation_key="car_detection", + native_value_fn=lambda data: data.is_car_detection_on, + turn_off_fn=lambda session: set_motion_detection(session, "carEnable", False), + turn_on_fn=lambda session: set_motion_detection(session, "carEnable", True), + exists_fn=lambda coordinator: coordinator.data.supports_car_adjustment, + ), + FoscamSwitchEntityDescription( + key="human_detection", + translation_key="human_detection", + native_value_fn=lambda data: data.is_human_detection_on, + turn_off_fn=lambda session: set_motion_detection(session, "humanEnable", False), + turn_on_fn=lambda session: set_motion_detection(session, "humanEnable", True), ), ] @@ -122,24 +156,11 @@ async def async_setup_entry( coordinator = config_entry.runtime_data - 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) + async_add_entities( + FoscamGenericSwitch(coordinator, description) + for description in SWITCH_DESCRIPTIONS + if description.exists_fn(coordinator) + ) class FoscamGenericSwitch(FoscamEntity, SwitchEntity): diff --git a/tests/components/foscam/conftest.py b/tests/components/foscam/conftest.py index a7a5b1abe48..f18f010f59c 100644 --- a/tests/components/foscam/conftest.py +++ b/tests/components/foscam/conftest.py @@ -79,11 +79,16 @@ def setup_mock_foscam_camera(mock_foscam_camera): 0, { "swCapabilities1": "100", - "swCapbilities2": "100", - "swCapbilities3": "100", - "swCapbilities4": "100", + "swCapabilities2": "768", + "swCapabilities3": "100", + "swCapabilities4": "100", }, ) + mock_foscam_camera.get_motion_detect_config.return_value = ( + 0, + {"petEnable": "1", "carEnable": "1", "humanEnable": "1"}, + ) + return mock_foscam_camera mock_foscam_camera.side_effect = configure_mock_on_init diff --git a/tests/components/foscam/snapshots/test_switch.ambr b/tests/components/foscam/snapshots/test_switch.ambr index f48df6b65e6..0945b657468 100644 --- a/tests/components/foscam/snapshots/test_switch.ambr +++ b/tests/components/foscam/snapshots/test_switch.ambr @@ -1,4 +1,52 @@ # serializer version: 1 +# name: test_entities[switch.mock_title_car_detection-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_car_detection', + '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': 'Car detection', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'car_detection', + 'unique_id': '123ABC_car_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_car_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Car detection', + }), + 'context': , + 'entity_id': 'switch.mock_title_car_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_entities[switch.mock_title_flip-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -47,6 +95,54 @@ 'state': 'off', }) # --- +# name: test_entities[switch.mock_title_human_detection-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_human_detection', + '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': 'Human detection', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'human_detection', + 'unique_id': '123ABC_human_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_human_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Human detection', + }), + 'context': , + 'entity_id': 'switch.mock_title_human_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_entities[switch.mock_title_infrared_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -191,6 +287,54 @@ 'state': 'off', }) # --- +# name: test_entities[switch.mock_title_pet_detection-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_pet_detection', + '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': 'Pet detection', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pet_detection', + 'unique_id': '123ABC_pet_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_pet_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Pet detection', + }), + 'context': , + 'entity_id': 'switch.mock_title_pet_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_entities[switch.mock_title_siren_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({