From 5505c0d87360964f8aa59ef154efbaebe42cb044 Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Mon, 6 Nov 2023 08:59:13 +0000 Subject: [PATCH 1/5] Add Dahua floodlight recognition --- custom_components/dahua/__init__.py | 17 +++++++++-------- custom_components/dahua/light.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/custom_components/dahua/__init__.py b/custom_components/dahua/__init__.py index 2597430..79f361a 100755 --- a/custom_components/dahua/__init__.py +++ b/custom_components/dahua/__init__.py @@ -253,8 +253,8 @@ async def _async_update_data(self): is_doorbell = self.is_doorbell() _LOGGER.info("Device is a doorbell=%s", is_doorbell) - is_amcrest_flood_light = self.is_amcrest_flood_light() - _LOGGER.info("Device is an Amcrest floodlight=%s", is_amcrest_flood_light) + is_flood_light = self.is_flood_light() + _LOGGER.info("Device is a floodlight=%s", is_flood_light) try: await self.client.async_get_config_lighting(self._channel, self._profile_mode) @@ -327,7 +327,7 @@ async def _async_update_data(self): if result is not None: data.update(result) - if self.supports_security_light() or self.is_amcrest_flood_light(): + if self.supports_security_light() or self.is_flood_light(): light_v2 = await self.client.async_get_lighting_v2() if light_v2 is not None: data.update(light_v2) @@ -541,9 +541,10 @@ def is_amcrest_doorbell(self) -> bool: """ Returns true if this is an Amcrest doorbell """ return self.model.upper().startswith("AD") - def is_amcrest_flood_light(self) -> bool: - """ Returns true if this camera is an Amcrest Floodlight camera (eg.ASH26-W) """ - return self.model.upper().startswith("ASH26") + def is_flood_light(self) -> bool: + """ Returns true if this camera is an floodlight camera (eg.ASH26-W) """ + m = self.model.upper() + return m.startswith("ASH26") or "L26N" in m or "L46N" in m def supports_infrared_light(self) -> bool: """ @@ -560,7 +561,7 @@ def supports_illuminator(self) -> bool: IPC-HDW3849HP-AS-PV does """ return not ( - self.is_amcrest_doorbell() or self.is_amcrest_flood_light()) and "table.Lighting_V2[{0}][0][0].Mode".format( + self.is_amcrest_doorbell() or self.is_flood_light()) and "table.Lighting_V2[{0}][0][0].Mode".format( self._channel) in self.data def is_motion_detection_enabled(self) -> bool: @@ -629,7 +630,7 @@ def is_illuminator_on(self) -> bool: return self.data.get("table.Lighting_V2[{0}][{1}][0].Mode".format(self._channel, profile_mode), "") == "Manual" - def is_amcrest_flood_light_on(self) -> bool: + def is_flood_light_on(self) -> bool: """Return true if the amcrest flood light light is on""" # profile_mode 0=day, 1=night, 2=scene profile_mode = self.get_profile_mode() diff --git a/custom_components/dahua/light.py b/custom_components/dahua/light.py index dca813e..eb8d939 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): if coordinator.supports_illuminator(): entities.append(DahuaIlluminator(coordinator, entry, "Illuminator")) - if coordinator.is_amcrest_flood_light(): + if coordinator.is_flood_light(): entities.append(AmcrestFloodLight(coordinator, entry, "Flood Light")) if coordinator.supports_security_light() and not coordinator.is_amcrest_doorbell(): @@ -233,7 +233,7 @@ def unique_id(self): @property def is_on(self): """Return true if the light is on""" - return self._coordinator.is_amcrest_flood_light_on() + return self._coordinator.is_flood_light_on() @property def supported_features(self): From 19862a3641846f43f2dfb209b245091e0fa3eb73 Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Tue, 7 Nov 2023 16:36:33 +0000 Subject: [PATCH 2/5] Add coaxial calls for newer floods --- custom_components/dahua/__init__.py | 18 ++++++++++------ custom_components/dahua/client.py | 8 +++---- custom_components/dahua/light.py | 33 +++++++++++++++++++---------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/custom_components/dahua/__init__.py b/custom_components/dahua/__init__.py index 79f361a..af710cd 100755 --- a/custom_components/dahua/__init__.py +++ b/custom_components/dahua/__init__.py @@ -522,7 +522,8 @@ def supports_siren(self) -> bool: Returns true if this camera has a siren. For example, the IPC-HDW3849HP-AS-PV does https://dahuawiki.com/Template:NameConvention """ - return "-AS-PV" in self.model + m = self.model.upper() + return "-AS-PV" in m or "L46N" in m or m.startswith("W452ASD") def supports_security_light(self) -> bool: """ @@ -544,7 +545,7 @@ def is_amcrest_doorbell(self) -> bool: def is_flood_light(self) -> bool: """ Returns true if this camera is an floodlight camera (eg.ASH26-W) """ m = self.model.upper() - return m.startswith("ASH26") or "L26N" in m or "L46N" in m + return m.startswith("ASH26") or "L26N" in m or "L46N" in m or m.startswith("V261LC") or m.startswith("W452ASD") def supports_infrared_light(self) -> bool: """ @@ -631,11 +632,16 @@ def is_illuminator_on(self) -> bool: return self.data.get("table.Lighting_V2[{0}][{1}][0].Mode".format(self._channel, profile_mode), "") == "Manual" def is_flood_light_on(self) -> bool: - """Return true if the amcrest flood light light is on""" - # profile_mode 0=day, 1=night, 2=scene - profile_mode = self.get_profile_mode() - return self.data.get(f'table.Lighting_V2[{self._channel}][{profile_mode}][1].Mode') == "Manual" + if self._supports_coaxial_control: + #'coaxialControlIO.cgi?action=getStatus&channel=1' + return self.data.get("status.status.WhiteLight", "") == "On" + else: + """Return true if the amcrest flood light light is on""" + # profile_mode 0=day, 1=night, 2=scene + profile_mode = self.get_profile_mode() + + return self.data.get(f'table.Lighting_V2[{self._channel}][{profile_mode}][1].Mode') == "Manual" def is_ring_light_on(self) -> bool: """Return true if ring light is on for an Amcrest Doorbell""" diff --git a/custom_components/dahua/client.py b/custom_components/dahua/client.py index c8c0519..1ce72fc 100755 --- a/custom_components/dahua/client.py +++ b/custom_components/dahua/client.py @@ -365,7 +365,7 @@ async def async_setprivacymask(self, index: int, enabled: bool): index, str(enabled).lower() ) return await self.get(url, True) - + async def async_set_night_switch_mode(self, channel: int, mode: str): """ async_set_night_switch_mode is the same as async_set_video_profile_mode when accessing the camera @@ -469,10 +469,10 @@ async def async_set_lighting_v2(self, channel: int, enabled: bool, brightness: i _LOGGER.debug("Turning light on: %s", url) return await self.get(url) - # async def async_set_lighting_v2_for_amcrest_flood_lights(self, channel: int, enabled: bool, brightness: int, profile_mode: str) -> dict: - async def async_set_lighting_v2_for_amcrest_flood_lights(self, channel: int, enabled: bool, profile_mode: str) -> dict: + # async def async_set_lighting_v2_for_flood_lights(self, channel: int, enabled: bool, brightness: int, profile_mode: str) -> dict: + async def async_set_lighting_v2_for_flood_lights(self, channel: int, enabled: bool, profile_mode: str) -> dict: """ - async_set_lighting_v2_for_amcrest_floodlights will turn on or off the flood light on the camera. If turning on, the brightness will be used. + async_set_lighting_v2_for_floodlights will turn on or off the flood light on the camera. If turning on, the brightness will be used. brightness is in the range of 0 to 100 inclusive where 100 is the brightest. NOTE: While the flood lights do support an auto or "smart" mode, the api does not handle this change properly. If one wishes to make the change back to auto, it must be done in the 'Amcrest Smart Home' smartphone app. diff --git a/custom_components/dahua/light.py b/custom_components/dahua/light.py index eb8d939..d0441bc 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): entities.append(DahuaIlluminator(coordinator, entry, "Illuminator")) if coordinator.is_flood_light(): - entities.append(AmcrestFloodLight(coordinator, entry, "Flood Light")) + entities.append(FloodLight(coordinator, entry, "Flood Light")) if coordinator.supports_security_light() and not coordinator.is_amcrest_doorbell(): # The Amcrest doorbell works a little different and is added in select.py @@ -205,9 +205,9 @@ async def async_turn_off(self, **kwargs): await self._coordinator.async_refresh() -class AmcrestFloodLight(DahuaBaseEntity, LightEntity): +class FloodLight(DahuaBaseEntity, LightEntity): """ - Representation of a Amcrest Flood Light (for cameras that have them) + Representation of a Amcrest, Dahua, and Lorex Flood Light (for cameras that have them) Unlike the 'Dahua Illuminator', Amcrest Flood Lights do not play nicely with adjusting the 'White Light' brightness. """ @@ -247,17 +247,28 @@ def should_poll(self): async def async_turn_on(self, **kwargs): """Turn the light on""" - channel = self._coordinator.get_channel() - profile_mode = self._coordinator.get_profile_mode() - await self._coordinator.client.async_set_lighting_v2_for_amcrest_flood_lights(channel, True, profile_mode) - await self._coordinator.async_refresh() + if self._coordinator._supports_coaxial_control: + channel = self._coordinator.get_channel() + await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, True) + await self._coordinator.async_refresh() + + else: + channel = self._coordinator.get_channel() + profile_mode = self._coordinator.get_profile_mode() + await self._coordinator.client.async_set_lighting_v2_for_flood_lights(channel, True, profile_mode) + await self._coordinator.async_refresh() async def async_turn_off(self, **kwargs): """Turn the light off""" - channel = self._coordinator.get_channel() - profile_mode = self._coordinator.get_profile_mode() - await self._coordinator.client.async_set_lighting_v2_for_amcrest_flood_lights(channel, False, profile_mode) - await self._coordinator.async_refresh() + if self._coordinator._supports_coaxial_control: + channel = self._coordinator.get_channel() + await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, False) + await self._coordinator.async_refresh() + else: + channel = self._coordinator.get_channel() + profile_mode = self._coordinator.get_profile_mode() + await self._coordinator.client.async_set_lighting_v2_for_flood_lights(channel, False, profile_mode) + await self._coordinator.async_refresh() class DahuaSecurityLight(DahuaBaseEntity, LightEntity): From 35568741066df60dc92b62a5064d11b5999027bd Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Tue, 7 Nov 2023 22:36:53 +0000 Subject: [PATCH 3/5] FloodLightMode stored on floodlight on/off --- custom_components/dahua/client.py | 17 +++++++++++++++++ custom_components/dahua/light.py | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/custom_components/dahua/client.py b/custom_components/dahua/client.py index 1ce72fc..7327cda 100755 --- a/custom_components/dahua/client.py +++ b/custom_components/dahua/client.py @@ -306,6 +306,23 @@ async def async_get_light_global_enabled(self) -> dict: url = "/cgi-bin/configManager.cgi?action=getConfig&name=LightGlobal[0].Enable" return await self.get(url) + async def async_get_floodlightmode(self) -> dict: + """ async_get_config_floodlightmode gets floodlight mode """ + url = "/cgi-bin/configManager.cgi?action=getConfig&name=FloodLightMode.Mode" + try: + return await self.async_get_config("FloodLightMode.Mode") + except aiohttp.ClientResponseError as e: + return {} + + async def async_set_floodlightmode(self, mode: int) -> dict: + """ async_set_floodlightmode will set the floodlight lighting control """ + # 1 - Motion Acvtivation + # 2 - Manual (for manual switching) + # 3 - Schedule + # 4 - PIR + url = "/cgi-bin/configManager.cgi?action=setConfig&FloodLightMode.Mode={mode}".format(mode=mode) + return await self.get(url) + async def async_set_lighting_v1(self, channel: int, enabled: bool, brightness: int) -> dict: """ async_get_lighting_v1 will turn the IR light (InfraRed light) on or off """ # on = Manual, off = Off diff --git a/custom_components/dahua/light.py b/custom_components/dahua/light.py index d0441bc..51b9dd5 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -216,6 +216,7 @@ def __init__(self, coordinator: DahuaDataUpdateCoordinator, entry, name): super().__init__(coordinator, entry) self._name = name self._coordinator = coordinator + self._store_mode = "2" @property def name(self): @@ -249,6 +250,8 @@ async def async_turn_on(self, **kwargs): """Turn the light on""" if self._coordinator._supports_coaxial_control: channel = self._coordinator.get_channel() + self._store_mode = await self._coordinator.client.async_get_floodlightmode() + await self._coordinator.client.async_set_floodlightmode(2) await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, True) await self._coordinator.async_refresh() @@ -263,6 +266,7 @@ async def async_turn_off(self, **kwargs): if self._coordinator._supports_coaxial_control: channel = self._coordinator.get_channel() await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, False) + await self._coordinator.client.async_set_floodlightmode(self._store_mode) await self._coordinator.async_refresh() else: channel = self._coordinator.get_channel() From 4383af379673fcff5eb13f4407a47ddb52af0735 Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Thu, 9 Nov 2023 08:54:37 -0800 Subject: [PATCH 4/5] Store floodlight state in coordinator --- custom_components/dahua/__init__.py | 2 ++ custom_components/dahua/client.py | 2 +- custom_components/dahua/light.py | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/dahua/__init__.py b/custom_components/dahua/__init__.py index af710cd..44bd490 100755 --- a/custom_components/dahua/__init__.py +++ b/custom_components/dahua/__init__.py @@ -155,6 +155,8 @@ def __init__(self, hass: HomeAssistant, events: list, address: str, port: int, r # If cleared the time will be 0. The time unit is seconds epoch self._dahua_event_timestamp: Dict[str, int] = dict() + self._floodlight_mode = 2 + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL_SECONDS) async def async_start_event_listener(self): diff --git a/custom_components/dahua/client.py b/custom_components/dahua/client.py index 7327cda..775db22 100755 --- a/custom_components/dahua/client.py +++ b/custom_components/dahua/client.py @@ -312,7 +312,7 @@ async def async_get_floodlightmode(self) -> dict: try: return await self.async_get_config("FloodLightMode.Mode") except aiohttp.ClientResponseError as e: - return {} + return 2 async def async_set_floodlightmode(self, mode: int) -> dict: """ async_set_floodlightmode will set the floodlight lighting control """ diff --git a/custom_components/dahua/light.py b/custom_components/dahua/light.py index 51b9dd5..18baf26 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -216,7 +216,6 @@ def __init__(self, coordinator: DahuaDataUpdateCoordinator, entry, name): super().__init__(coordinator, entry) self._name = name self._coordinator = coordinator - self._store_mode = "2" @property def name(self): @@ -250,7 +249,7 @@ async def async_turn_on(self, **kwargs): """Turn the light on""" if self._coordinator._supports_coaxial_control: channel = self._coordinator.get_channel() - self._store_mode = await self._coordinator.client.async_get_floodlightmode() + self._coordinator._floodlight_mode = await self._coordinator.client.async_get_floodlightmode() await self._coordinator.client.async_set_floodlightmode(2) await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, True) await self._coordinator.async_refresh() @@ -266,7 +265,7 @@ async def async_turn_off(self, **kwargs): if self._coordinator._supports_coaxial_control: channel = self._coordinator.get_channel() await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, False) - await self._coordinator.client.async_set_floodlightmode(self._store_mode) + await self._coordinator.client.async_set_floodlightmode(self._coordinator._floodlight_mode) await self._coordinator.async_refresh() else: channel = self._coordinator.get_channel() From 9816e07c699b060807a8487fda93c6c3eb728424 Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Sun, 12 Nov 2023 15:38:24 -0800 Subject: [PATCH 5/5] Specify support for floodlightmode vs coaxial API for floodlights --- custom_components/dahua/__init__.py | 9 ++++++++- custom_components/dahua/light.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/custom_components/dahua/__init__.py b/custom_components/dahua/__init__.py index 44bd490..e3ab213 100755 --- a/custom_components/dahua/__init__.py +++ b/custom_components/dahua/__init__.py @@ -121,6 +121,7 @@ def __init__(self, hass: HomeAssistant, events: list, address: str, port: int, r self._supports_disarming_linkage = False self._supports_smart_motion_detection = False self._supports_lighting = False + self._supports_floodlightmode = False self._serial_number: str self._profile_mode = "0" self._supports_profile_mode = False @@ -258,6 +259,8 @@ async def _async_update_data(self): is_flood_light = self.is_flood_light() _LOGGER.info("Device is a floodlight=%s", is_flood_light) + self._supports_floodlightmode = self.supports_floodlightmode() + try: await self.client.async_get_config_lighting(self._channel, self._profile_mode) self._supports_lighting = True @@ -549,6 +552,10 @@ def is_flood_light(self) -> bool: m = self.model.upper() return m.startswith("ASH26") or "L26N" in m or "L46N" in m or m.startswith("V261LC") or m.startswith("W452ASD") + def supports_floodlightmode(self) -> bool: + """ Returns true if this camera supports floodlight mode """ + return "W452ASD" in self.model.upper() or "L46N" in self.model.upper() + def supports_infrared_light(self) -> bool: """ Returns true if this camera has an infrared light. For example, the IPC-HDW3849HP-AS-PV does not, but most @@ -635,7 +642,7 @@ def is_illuminator_on(self) -> bool: def is_flood_light_on(self) -> bool: - if self._supports_coaxial_control: + if self._supports_floodlightmode: #'coaxialControlIO.cgi?action=getStatus&channel=1' return self.data.get("status.status.WhiteLight", "") == "On" else: diff --git a/custom_components/dahua/light.py b/custom_components/dahua/light.py index 18baf26..1e09e66 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -247,7 +247,7 @@ def should_poll(self): async def async_turn_on(self, **kwargs): """Turn the light on""" - if self._coordinator._supports_coaxial_control: + if self._coordinator._supports_floodlightmode: channel = self._coordinator.get_channel() self._coordinator._floodlight_mode = await self._coordinator.client.async_get_floodlightmode() await self._coordinator.client.async_set_floodlightmode(2) @@ -262,7 +262,7 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the light off""" - if self._coordinator._supports_coaxial_control: + if self._coordinator._supports_floodlightmode: channel = self._coordinator.get_channel() await self._coordinator.client.async_set_coaxial_control_state(channel, SECURITY_LIGHT_TYPE, False) await self._coordinator.client.async_set_floodlightmode(self._coordinator._floodlight_mode)