From 225792888242b645cbba6aa771e364ef3eb0263d Mon Sep 17 00:00:00 2001 From: Chris Hamilton Date: Fri, 10 Nov 2023 10:50:32 -0800 Subject: [PATCH] Add support for L46N floodlights and possibly the Lorex equivalent (#313) * Add Dahua floodlight recognition * Add coaxial calls for newer floods * FloodLightMode stored on floodlight on/off * Store floodlight state in coordinator --- custom_components/dahua/__init__.py | 35 +++++++++++++++---------- custom_components/dahua/client.py | 25 +++++++++++++++--- custom_components/dahua/light.py | 40 +++++++++++++++++++---------- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/custom_components/dahua/__init__.py b/custom_components/dahua/__init__.py index 2597430..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): @@ -253,8 +255,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 +329,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) @@ -522,7 +524,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: """ @@ -541,9 +544,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 or m.startswith("V261LC") or m.startswith("W452ASD") def supports_infrared_light(self) -> bool: """ @@ -560,7 +564,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,12 +633,17 @@ 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: - """Return true if the amcrest flood light light is on""" - # profile_mode 0=day, 1=night, 2=scene - profile_mode = self.get_profile_mode() + def is_flood_light_on(self) -> bool: + + 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" + 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..775db22 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 2 + + 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 @@ -365,7 +382,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 +486,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 dca813e..18baf26 100755 --- a/custom_components/dahua/light.py +++ b/custom_components/dahua/light.py @@ -30,8 +30,8 @@ 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(): - entities.append(AmcrestFloodLight(coordinator, entry, "Flood Light")) + if coordinator.is_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. """ @@ -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): @@ -247,17 +247,31 @@ 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() + 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() + + 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.client.async_set_floodlightmode(self._coordinator._floodlight_mode) + 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):