Skip to content

Commit

Permalink
Merge pull request #716 from JurajNyiri/5.8.2
Browse files Browse the repository at this point in the history
Add: Switches for alert event types, Siren alternative trigger, SirenType for more cameras
  • Loading branch information
JurajNyiri authored Nov 28, 2024
2 parents 1e3bd35 + 972a783 commit 243c346
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This custom component creates:
- Binary sensor for motion after the motion is detected for the first time
- Light entity, if the camera supports a floodlight switch
- Buttons for Calibrate, Format, Manual Alarm start & stop, Moving the camera, Reboot and syncing time
- Switch entities for Auto track, Flip setting, LED Indicator, Lens Distortion Correction, (Rich) Notifications, Recording, Microphone Mute, Microphone Noise Cancelling, Automatically Upgrade Firmware, HDR mode and Privacy mode
- Switch entities for Auto track, Flip setting, LED Indicator, Lens Distortion Correction, (Rich) Notifications, Recording, Microphone Mute, Microphone Noise Cancelling, Automatically Upgrade Firmware, HDR mode, Alarm Trigger Event types and Privacy mode
- Select entities for Automatic Alarm, Light Frequency, Motion Detection, Night Vision, Spotlight Intensity, Alarm Type and Move to Preset
- Number entity for Movement Angle, Speaker Volume, Microphone Volume, Spotlight Intensity, Siren Volume, Siren Duration and Motion Detection Digital Sensitivity
- Media Source for browsing and playing recordings stored on camera
Expand Down
86 changes: 83 additions & 3 deletions custom_components/tapo_control/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .const import DOMAIN, LOGGER
from .tapo.entities import TapoButtonEntity
from .utils import syncTime, check_and_create
from .utils import syncTime, check_and_create, result_has_error


async def async_setup_entry(
Expand Down Expand Up @@ -118,11 +118,51 @@ def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
)

async def async_press(self) -> None:
await self._hass.async_add_executor_job(self._controller.startManualAlarm)
result = False
result2 = False
try:
result = await self._hass.async_add_executor_job(
self._controller.startManualAlarm,
)
except Exception as e:
LOGGER.debug(e)

try:
result2 = await self._hass.async_add_executor_job(
self._controller.setSirenStatus, True
)
except Exception as e:
LOGGER.debug(e)

if result_has_error(result) and result_has_error(result2):
if self.sirenType is not None:
try:
result3 = await self._hass.async_add_executor_job(
self._controller.testUsrDefAudio, self.sirenType, True
)
if result_has_error(result3):
raise Exception("Camera does not support triggering the siren.")
except Exception:
raise Exception("Camera does not support triggering the siren.")
else:
raise Exception("Camera does not support triggering the siren.")

def updateTapo(self, camData):
if not camData or camData["privacy_mode"] == "on":
self.camData = STATE_UNAVAILABLE
else:
if (
"alarm_config" in camData
and camData["alarm_config"]
and "siren_type" in camData["alarm_config"]
and camData["alarm_config"]["siren_type"]
):
self.sirenType = camData["alarm_config"]["siren_type"]


class TapoStopManualAlarmButton(TapoButtonEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
self.sirenType = None
TapoButtonEntity.__init__(
self,
"Manual Alarm Stop",
Expand All @@ -132,7 +172,47 @@ def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
)

async def async_press(self) -> None:
await self._hass.async_add_executor_job(self._controller.stopManualAlarm)
result = False
result2 = False
try:
result = await self._hass.async_add_executor_job(
self._controller.stopManualAlarm,
)
except Exception as e:
LOGGER.debug(e)

try:
result2 = await self._hass.async_add_executor_job(
self._controller.setSirenStatus, False
)
except Exception as e:
LOGGER.debug(e)

if result_has_error(result) and result_has_error(result2):
if self.sirenType is not None:
try:
result3 = await self._hass.async_add_executor_job(
self._controller.testUsrDefAudio, self.sirenType, False
)
if result_has_error(result3):
self._attr_available = False
raise Exception("Camera does not support triggering the siren.")
except Exception:
raise Exception("Camera does not support triggering the siren.")
else:
raise Exception("Camera does not support triggering the siren.")

def updateTapo(self, camData):
if not camData or camData["privacy_mode"] == "on":
self.camData = STATE_UNAVAILABLE
else:
if (
"alarm_config" in camData
and camData["alarm_config"]
and "siren_type" in camData["alarm_config"]
and camData["alarm_config"]["siren_type"]
):
self.sirenType = camData["alarm_config"]["siren_type"]


class TapoCalibrateButton(TapoButtonEntity):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/tapo_control/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from homeassistant.helpers import config_validation as cv

PYTAPO_REQUIRED_VERSION = "3.3.32"
PYTAPO_REQUIRED_VERSION = "3.3.36"
DOMAIN = "tapo_control"
BRAND = "TP-Link"
ALARM_MODE = "alarm_mode"
Expand Down
4 changes: 2 additions & 2 deletions custom_components/tapo_control/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"codeowners": [
"@JurajNyiri"
],
"version": "5.8.0",
"version": "5.8.2",
"requirements": [
"pytapo==3.3.32"
"pytapo==3.3.36"
],
"dependencies": [
"ffmpeg",
Expand Down
20 changes: 13 additions & 7 deletions custom_components/tapo_control/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ async def setupEntities(entry):
if tapoAlertTypeSelect:
LOGGER.debug("Adding tapoAlertTypeSelect...")
selects.append(tapoAlertTypeSelect)
else:
LOGGER.debug("Adding tapoAlertTypeSelect with start ID 0...")
selects.append(TapoAlertTypeSelect(entry, hass, config_entry, 0))

tapoMotionDetectionSelect = await check_and_create(
entry, hass, TapoMotionDetectionSelect, "getMotionDetection", config_entry
Expand Down Expand Up @@ -1092,9 +1095,9 @@ async def async_select_option(self, option: str) -> None:


class TapoAlertTypeSelect(TapoSelectEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry, startID=10):
self.hub = entry["camData"]["alarm_is_hubSiren"]
self.startID = 10
self.startID = startID
self.alarm_siren_type_list = entry["camData"]["alarm_siren_type_list"]
self.typeOfAlarm = entry["camData"]["alarm_config"]["typeOfAlarm"]

Expand All @@ -1116,17 +1119,20 @@ def updateTapo(self, camData):
else:
self._attr_options = camData["alarm_siren_type_list"]
self.user_sounds = {}
for user_sound in camData["alarm_user_sounds"]:
if "name" in user_sound:
self._attr_options.append(user_sound["name"])
if "id" in user_sound:
self.user_sounds[user_sound["id"]] = user_sound["name"]
if camData["alarm_user_sounds"] is not None:
for user_sound in camData["alarm_user_sounds"]:
if "name" in user_sound:
self._attr_options.append(user_sound["name"])
if "id" in user_sound:
self.user_sounds[user_sound["id"]] = user_sound["name"]

self.alarm_enabled = camData["alarm_config"]["automatic"] == "on"
self.alarm_mode = camData["alarm_config"]["mode"]
currentSirenType = int(camData["alarm_config"]["siren_type"])
if currentSirenType == 0:
self._attr_current_option = camData["alarm_siren_type_list"][0]
elif currentSirenType == 1:
self._attr_current_option = camData["alarm_siren_type_list"][1]
elif currentSirenType < self.startID:
# on these cameras, the 0 is the first entry, but then it starts from 3
# and it has 3 and 4 values, assuming -2 for the rest
Expand Down
23 changes: 1 addition & 22 deletions custom_components/tapo_control/siren.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .const import DOMAIN, LOGGER
from .tapo.entities import TapoEntity
from .utils import check_and_create
from .utils import check_and_create, result_has_error


async def async_setup_entry(
Expand Down Expand Up @@ -164,24 +164,3 @@ def updateTapo(self, camData):
else:
self._attr_available = True
self._is_on = camData["alarm_status"] == "on"


def result_has_error(result):
if (
result is not False
and "result" in result
and "responses" in result["result"]
and any(
map(
lambda x: "error_code" not in x or x["error_code"] == 0,
result["result"]["responses"],
)
)
):
return False
if result is not False and (
"error_code" not in result or result["error_code"] == 0
):
return False
else:
return True
59 changes: 59 additions & 0 deletions custom_components/tapo_control/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ async def setupEntities(entry):
await entry_storage.async_save({ENABLE_MEDIA_SYNC: False})
entry_stored_data = await entry_storage.async_load()

if (
"alert_event_types" in entry["camData"]
and entry["camData"]["alert_event_types"]
):
for alertEventType in entry["camData"]["alert_event_types"]:
switches.append(
TapoAlarmEventTypeSwitch(
entry, hass, config_entry, alertEventType["name"]
)
)

tapoEnableMediaSyncSwitch = TapoEnableMediaSyncSwitch(
entry,
hass,
Expand Down Expand Up @@ -535,6 +546,54 @@ def updateTapo(self, camData):
self._attr_state = "on" if self._attr_is_on else "off"


class TapoAlarmEventTypeSwitch(TapoSwitchEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry, eventType: str):
self.eventType = eventType
TapoSwitchEntity.__init__(
self,
f"Trigger alarm on {eventType}",
entry,
hass,
config_entry,
"mdi:exclamation",
)

async def async_update(self) -> None:
await self._coordinator.async_request_refresh()

async def async_turn_on(self) -> None:
result = await self._hass.async_add_executor_job(
self._controller.setAlertEventType,
self.eventType,
True,
)
if "error_code" not in result or result["error_code"] == 0:
self._attr_state = "on"
self.async_write_ha_state()
await self._coordinator.async_request_refresh()

async def async_turn_off(self) -> None:
result = await self._hass.async_add_executor_job(
self._controller.setAlertEventType,
self.eventType,
False,
)
if "error_code" not in result or result["error_code"] == 0:
self._attr_state = "on"
self.async_write_ha_state()
await self._coordinator.async_request_refresh()

def updateTapo(self, camData):
if not camData:
self._attr_state = STATE_UNAVAILABLE
else:
if "alert_event_types" in camData and camData["alert_event_types"]:
for alertEventType in camData["alert_event_types"]:
if alertEventType["name"] == self.eventType:
self._attr_is_on = alertEventType["enabled"] == "on"
self._attr_state = "on" if self._attr_is_on else "off"


class TapoLensDistortionCorrectionSwitch(TapoSwitchEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
TapoSwitchEntity.__init__(
Expand Down
75 changes: 55 additions & 20 deletions custom_components/tapo_control/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,27 @@ async def isRtspStreamWorking(hass, host, username, password, full_url=""):
return not image == b""


def result_has_error(result):
if (
result is not False
and "result" in result
and "responses" in result["result"]
and any(
map(
lambda x: "error_code" not in x or x["error_code"] == 0,
result["result"]["responses"],
)
)
):
return False
if result is not False and (
"error_code" not in result or result["error_code"] == 0
):
return False
else:
return True


async def initOnvifEvents(hass, host, username, password):
device = ONVIFCamera(
host,
Expand Down Expand Up @@ -741,6 +762,12 @@ async def getCamData(hass, controller):
timezone_timezone = None
camData["timezone_timezone"] = timezone_timezone

try:
alert_event_types = data["getAlertEventType"][0]["msg_alarm"]["msg_alarm_type"]
except Exception:
alert_event_types = None
camData["alert_event_types"] = alert_event_types

try:
timezone_zone_id = data["getTimezone"][0]["system"]["basic"]["zone_id"]
except Exception:
Expand Down Expand Up @@ -1144,33 +1171,41 @@ async def getCamData(hass, controller):
except Exception as err:
LOGGER.error(f"getSirenTypeList unexpected error {err=}, {type(err)=}")

if len(alarmSirenTypeList) == 0:
# Some cameras have hardcoded 0 and 1 values (Siren, Tone)
alarmSirenTypeList.append("Siren")
alarmSirenTypeList.append("Tone")

alarm_user_sounds = None
try:
if (
data["getAlertConfig"][0] is not False
and "msg_alarm" in data["getAlertConfig"][0]
and "usr_def_audio" in data["getAlertConfig"][0]["msg_alarm"]
):
alarm_user_sounds = []
for alarm_sound in data["getAlertConfig"][0]["msg_alarm"]["usr_def_audio"]:
first_key = next(iter(alarm_sound))
first_value = alarm_sound[first_key]
alarm_user_sounds.append(first_value)
for alertConfig in data["getAlertConfig"]:
if (
alertConfig is not False
and "msg_alarm" in alertConfig
and "usr_def_audio" in alertConfig["msg_alarm"]
and (alarm_user_sounds is None or len(alarm_user_sounds) == 0)
):
alarm_user_sounds = []
for alarm_sound in alertConfig["msg_alarm"]["usr_def_audio"]:
first_key = next(iter(alarm_sound))
first_value = alarm_sound[first_key]
alarm_user_sounds.append(first_value)
except Exception:
alarm_user_sounds = None

alarm_user_start_id = None
try:
if (
data["getAlertConfig"][0] is not False
and "msg_alarm" in data["getAlertConfig"][0]
and "capability" in data["getAlertConfig"][0]["msg_alarm"]
and "usr_def_start_file_id"
in data["getAlertConfig"][0]["msg_alarm"]["capability"]
):
alarm_user_start_id = data["getAlertConfig"][0]["msg_alarm"]["capability"][
"usr_def_start_file_id"
]
for alertConfig in data["getAlertConfig"]:
if (
alertConfig is not False
and "msg_alarm" in alertConfig
and "capability" in alertConfig["msg_alarm"]
and "usr_def_start_file_id" in alertConfig["msg_alarm"]["capability"]
and alarm_user_start_id is None
):
alarm_user_start_id = alertConfig["msg_alarm"]["capability"][
"usr_def_start_file_id"
]
except Exception:
alarm_user_start_id = None
camData["alarm_user_start_id"] = alarm_user_start_id
Expand Down

0 comments on commit 243c346

Please sign in to comment.