From d065f4b58bc4b19c2b2ecc6ae62888557d9c586d Mon Sep 17 00:00:00 2001 From: fuatakgun Date: Fri, 30 Dec 2022 21:14:30 +0100 Subject: [PATCH] feat: ringtone service and v5.3.1 --- README.md | 5 +- custom_components/eufy_security/__init__.py | 5 ++ .../eufy_security/alarm_control_panel.py | 5 ++ custom_components/eufy_security/const.py | 1 + .../eufy_security/coordinator.py | 3 - .../eufy_security_api/api_client.py | 84 +++++++++++-------- .../eufy_security/eufy_security_api/const.py | 2 + .../eufy_security_api/outgoing_message.py | 8 ++ .../eufy_security_api/product.py | 4 + custom_components/eufy_security/manifest.json | 2 +- custom_components/eufy_security/services.yaml | 39 +++++++++ 11 files changed, 117 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 5680d80..79bbe00 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Please check here: https://github.com/bropat/eufy-security-client#known-working- In upcoming steps, you are going to install at least one add-on and one integration. -In Home Assistant eco-system, if you are using Supervised or HASS OS based setup, you can use `Add-ons` page of Home Assistant to install these. If you are running Core or you don't have `Add-ons` option in your setup, you need to install the docker and run these containers yourself. You will see respective commands in respective steps. +In Home Assistant eco-system, if you are using Supervised or HASS OS based setup, you can use `Add-ons` page of Home Assistant to install these. If you are running Core or you don't have `Add-ons` option in your setup, you need to install the docker and run these containers yourself. You will see respective commands in respective steps. If you are interested in composing of your docker container, please check the end section This integration is not part of Home Assistant Core so you have to install this as a custom integration. There are two ways of doing this, either manaully downloading and copying files or using HACS (Home Assistant Community Store). I will be using HACS method here. @@ -165,7 +165,7 @@ cards: - `quick_response` - Send a quick response message for doorbell, you can get `voice_id` information from `Debug (device)` sensor attributes of device. - `snooze` - Snooze ongoing notification for a given duration. - Alarm Panel Services; - - There is an select entity called Guard Mode, it is one to one mapping of Eufy Security state. + - There is an select entity called Guard Mode, it is one to one mapping of Eufy Security state. Current Mode sensor is showing the current state of home base. - `trigger_base_alarm_with_duration` - Trigger alarm on station for a given duration - `reset_alarm` - Reset ongoing alarm for a given duration - `snooze` - Snooze ongoing notification for a given duration. @@ -177,6 +177,7 @@ cards: - `alarm_arm_custom3` - Switch to custom 3 - `geofence` - Switch to geofencing, this might not impact the state of panel given that it will chage its state based on geolocation via eufy app - `schedule` - Switch to custom 3, this might not impact the state of panel given that it will chage its state based on schedule via eufy app + - `chime` - Trigger a chime sound on base station (liked it) - I do not know exact list of ringtones available, try it yourself. # Example Automation ## Start streaming on camera, when there is a motion, this would generate a new thumbnail on Home Assistant diff --git a/custom_components/eufy_security/__init__.py b/custom_components/eufy_security/__init__.py index ba8cb73..5e296c5 100644 --- a/custom_components/eufy_security/__init__.py +++ b/custom_components/eufy_security/__init__.py @@ -29,8 +29,13 @@ async def handle_force_sync(call): coordinator: EufySecurityDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR] await coordinator.async_refresh() + async def handle_log_level(call): + coordinator: EufySecurityDataUpdateCoordinator = hass.data[DOMAIN][COORDINATOR] + await coordinator.api.set_log_level(call.data.get("log_level")) + hass.services.async_register(DOMAIN, "force_sync", handle_force_sync) hass.services.async_register(DOMAIN, "send_message", handle_send_message) + hass.services.async_register(DOMAIN, "set_log_level", handle_log_level) return True diff --git a/custom_components/eufy_security/alarm_control_panel.py b/custom_components/eufy_security/alarm_control_panel.py index de8d820..ecf6728 100644 --- a/custom_components/eufy_security/alarm_control_panel.py +++ b/custom_components/eufy_security/alarm_control_panel.py @@ -76,6 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn platform.async_register_entity_service("alarm_arm_custom3", {}, "async_alarm_arm_vacation") platform.async_register_entity_service("geofence", {}, "geofence") platform.async_register_entity_service("schedule", {}, "schedule") + platform.async_register_entity_service("chime", Schema.CHIME_SERVICE_SCHEMA.value, "chime") class EufySecurityAlarmControlPanel(AlarmControlPanelEntity, EufySecurityEntity): @@ -141,6 +142,10 @@ async def schedule(self) -> None: """switch to schedule mode""" await self._set_guard_mode(CurrentModeToState.SCHEDULE) + async def chime(self, ringtone: int) -> None: + """chime on alarm control panel""" + await self.product.chime(ringtone) + @property def state(self): alarm_delayed = get_child_value(self.product.properties, "alarmDelay", 0) diff --git a/custom_components/eufy_security/const.py b/custom_components/eufy_security/const.py index 6ed5ec8..8638b30 100644 --- a/custom_components/eufy_security/const.py +++ b/custom_components/eufy_security/const.py @@ -41,6 +41,7 @@ class Schema(Enum): TRIGGER_ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Required("duration"): cv.Number}) QUICK_RESPONSE_SERVICE_SCHEMA = make_entity_service_schema({vol.Required("voice_id"): cv.Number}) + CHIME_SERVICE_SCHEMA = make_entity_service_schema({vol.Required("ringtone"): cv.Number}) SNOOZE = make_entity_service_schema( { vol.Required("snooze_time"): cv.Number, diff --git a/custom_components/eufy_security/coordinator.py b/custom_components/eufy_security/coordinator.py index 39c9675..4cb307a 100644 --- a/custom_components/eufy_security/coordinator.py +++ b/custom_components/eufy_security/coordinator.py @@ -55,8 +55,5 @@ def platforms(self): return self._platforms async def _update_local(self): - # try: await self.api.poll_refresh() return self.data - # except Exception as exception: - # raise UpdateFailed() from exception diff --git a/custom_components/eufy_security/eufy_security_api/api_client.py b/custom_components/eufy_security/eufy_security_api/api_client.py index 59b62eb..386adce 100644 --- a/custom_components/eufy_security/eufy_security_api/api_client.py +++ b/custom_components/eufy_security/eufy_security_api/api_client.py @@ -63,38 +63,6 @@ async def connect(self): await self._set_schema(SCHEMA_VERSION) await self._set_products() - async def set_captcha_and_connect(self, captcha_id: str, captcha_input: str): - """Set captcha set products""" - await self._set_captcha(captcha_id, captcha_input) - await asyncio.sleep(10) - await self._set_products() - - async def set_mfa_and_connect(self, mfa_input: str): - """Set captcha set products""" - await self._set_mfa_code(mfa_input) - await asyncio.sleep(10) - await self._set_products() - - async def _set_captcha(self, captcha_id: str, captcha_input: str) -> None: - command_type = OutgoingMessageType.set_captcha - command = EventSourceType.driver.name + "." + command_type.name - await self._send_message_get_response( - OutgoingMessage(command_type, command=command, captcha_id=captcha_id, captcha_input=captcha_input) - ) - - async def _set_mfa_code(self, mfa_input: str) -> None: - command_type = OutgoingMessageType.set_verify_code - command = EventSourceType.driver.name + "." + command_type.name - await self._send_message_get_response(OutgoingMessage(command_type, command=command, verify_code=mfa_input)) - - async def _connect_driver(self) -> None: - command_type = OutgoingMessageType.driver_connect - command = EventSourceType.driver.name + "." + "connect" - await self._send_message_get_response(OutgoingMessage(command_type, command=command)) - - async def _set_schema(self, schema_version: int) -> None: - await self._send_message_get_response(OutgoingMessage(OutgoingMessageType.set_api_schema, schema_version=schema_version)) - async def _set_products(self) -> None: result = await self._send_message_get_response(OutgoingMessage(OutgoingMessageType.start_listening)) if result[MessageField.STATE.value][EventSourceType.driver.name][MessageField.CONNECTED.value] is False: @@ -142,6 +110,46 @@ async def _get_product(self, product_type: ProductType, products: list) -> dict: response[serial_no] = product return response + async def set_captcha_and_connect(self, captcha_id: str, captcha_input: str): + """Set captcha set products""" + await self._set_captcha(captcha_id, captcha_input) + await asyncio.sleep(10) + await self._set_products() + + async def set_mfa_and_connect(self, mfa_input: str): + """Set mfa code set products""" + await self._set_mfa_code(mfa_input) + await asyncio.sleep(10) + await self._set_products() + + # general api commands + async def _set_captcha(self, captcha_id: str, captcha_input: str) -> None: + command_type = OutgoingMessageType.set_captcha + command = EventSourceType.driver.name + "." + command_type.name + await self._send_message_get_response( + OutgoingMessage(command_type, command=command, captcha_id=captcha_id, captcha_input=captcha_input) + ) + + async def _set_mfa_code(self, mfa_input: str) -> None: + command_type = OutgoingMessageType.set_verify_code + command = EventSourceType.driver.name + "." + command_type.name + await self._send_message_get_response(OutgoingMessage(command_type, command=command, verify_code=mfa_input)) + + async def _connect_driver(self) -> None: + command_type = OutgoingMessageType.driver_connect + command = EventSourceType.driver.name + "." + "connect" + await self._send_message_get_response(OutgoingMessage(command_type, command=command)) + + async def _set_schema(self, schema_version: int) -> None: + await self._send_message_get_response(OutgoingMessage(OutgoingMessageType.set_api_schema, schema_version=schema_version)) + + async def set_log_level(self, log_level: str) -> None: + """set log level of websocket server""" + command_type = OutgoingMessageType.set_log_level + command = EventSourceType.driver.name + "." + "connect" + await self._send_message_get_response(OutgoingMessage(command_type, command=command, log_level=log_level)) + + # device and station functions async def _get_is_rtsp_streaming(self, product_type: ProductType, serial_no: str) -> bool: command_type = OutgoingMessageType.is_rtsp_livestreaming command = product_type.name + "." + command_type.name @@ -161,15 +169,21 @@ async def pan_and_tilt(self, product_type: ProductType, serial_no: str, directio await self._send_message_get_response(OutgoingMessage(command_type, command=command, serial_no=serial_no, direction=direction)) async def quick_response(self, product_type: ProductType, serial_no: str, voice_id: int) -> None: - """Process start pan tilt rotate zoom""" + """Process quick response for doorbell""" command_type = OutgoingMessageType.quick_response command = product_type.name + "." + command_type.name await self._send_message_get_response(OutgoingMessage(command_type, command=command, serial_no=serial_no, voice_id=voice_id)) + async def chime(self, product_type: ProductType, serial_no: str, ringtone: int) -> None: + """Process chme call""" + command_type = OutgoingMessageType.chime + command = product_type.name + "." + command_type.name + await self._send_message_get_response(OutgoingMessage(command_type, command=command, serial_no=serial_no, ringtone=ringtone)) + async def snooze( self, product_type: ProductType, serial_no: str, snooze_time: int, snooze_chime: bool, snooze_motion: bool, snooze_homebase: bool ) -> None: - """Process start pan tilt rotate zoom""" + """Process snooze for devices ans stations""" command_type = OutgoingMessageType.snooze command = product_type.name + "." + command_type.name await self._send_message_get_response( @@ -252,7 +266,7 @@ async def _get_commands(self, product_type: ProductType, serial_no: str) -> dict async def _on_message(self, message: dict) -> None: message_str = str(message)[0:200] - # message_str = str(message) + message_str = str(message) if "livestream video data" not in message_str and "livestream audio data" not in message_str: _LOGGER.debug(f"_on_message - {message_str}") if message[MessageField.TYPE.value] == IncomingMessageType.result.name: diff --git a/custom_components/eufy_security/eufy_security_api/const.py b/custom_components/eufy_security/eufy_security_api/const.py index e49657c..308d6e6 100644 --- a/custom_components/eufy_security/eufy_security_api/const.py +++ b/custom_components/eufy_security/eufy_security_api/const.py @@ -22,6 +22,7 @@ class MessageField(Enum): CONNECTED = "connected" SOURCE = "source" SCHEMA_VERSION = "schemaVersion" + LOG_LEVEL = "level" BIND_AT_RUNTIME = "bindAtRuntime" SERIAL_NUMBER = "serialNumber" PROPERTIES = "properties" @@ -57,6 +58,7 @@ class MessageField(Enum): CURRENT_MODE = "currentMode" GUARD_MODE = "guardMode" SECONDS = "seconds" + RINGTONE = "ringtone" # camera specific PICTURE_URL = "pictureUrl" diff --git a/custom_components/eufy_security/eufy_security_api/outgoing_message.py b/custom_components/eufy_security/eufy_security_api/outgoing_message.py index d64e3b0..9f2912e 100644 --- a/custom_components/eufy_security/eufy_security_api/outgoing_message.py +++ b/custom_components/eufy_security/eufy_security_api/outgoing_message.py @@ -24,6 +24,8 @@ class OutgoingMessageToParameter(Enum): snoozeChime = "snooze_chime" snoozeMotion = "snooze_motion" snoozeHomebase = "snooze_homebase" + level = "log_level" + ringtone = "ringtone" class OutgoingMessageType(Enum): @@ -31,6 +33,7 @@ class OutgoingMessageType(Enum): driver_connect = {MessageField.COMMAND.value: auto()} set_api_schema = {MessageField.COMMAND.value: auto(), MessageField.SCHEMA_VERSION.value: MessageField.BIND_AT_RUNTIME} + set_log_level = {MessageField.COMMAND.value: auto(), MessageField.LOG_LEVEL.value: MessageField.BIND_AT_RUNTIME} start_listening = {MessageField.COMMAND.value: auto()} get_properties_metadata = {MessageField.COMMAND.value: auto(), MessageField.SERIAL_NUMBER.value: MessageField.BIND_AT_RUNTIME} get_properties = {MessageField.COMMAND.value: auto(), MessageField.SERIAL_NUMBER.value: MessageField.BIND_AT_RUNTIME} @@ -79,6 +82,11 @@ class OutgoingMessageType(Enum): MessageField.SNOOZE_MOTION.value: MessageField.BIND_AT_RUNTIME, MessageField.SNOOZE_HOMEBASE.value: MessageField.BIND_AT_RUNTIME, } + chime = { + MessageField.COMMAND.value: auto(), + MessageField.SERIAL_NUMBER.value: MessageField.BIND_AT_RUNTIME, + MessageField.RINGTONE.value: MessageField.BIND_AT_RUNTIME, + } class OutgoingMessage: diff --git a/custom_components/eufy_security/eufy_security_api/product.py b/custom_components/eufy_security/eufy_security_api/product.py index d4c4805..82d4678 100644 --- a/custom_components/eufy_security/eufy_security_api/product.py +++ b/custom_components/eufy_security/eufy_security_api/product.py @@ -113,3 +113,7 @@ class Station(Product): def __init__(self, api, serial_no: str, properties: dict, metadata: dict, commands: []) -> None: super().__init__(api, ProductType.station, serial_no, properties, metadata, commands) + + async def chime(self, ringtone: int) -> None: + """Quick response message to camera""" + await self.api.chime(self.product_type, self.serial_no, ringtone) diff --git a/custom_components/eufy_security/manifest.json b/custom_components/eufy_security/manifest.json index fe5696a..413f68c 100644 --- a/custom_components/eufy_security/manifest.json +++ b/custom_components/eufy_security/manifest.json @@ -5,7 +5,7 @@ "issue_tracker": "https://github.com/fuatakgun/eufy_security/issues", "dependencies": ["ffmpeg", "stream"], "config_flow": true, - "version": "5.3.0", + "version": "5.3.1", "codeowners": ["@fuatakgun"], "requirements": ["websocket-client==1.1.0", "aiortsp==1.3.6"], "iot_class": "cloud_push", diff --git a/custom_components/eufy_security/services.yaml b/custom_components/eufy_security/services.yaml index b5660ce..e251a68 100644 --- a/custom_components/eufy_security/services.yaml +++ b/custom_components/eufy_security/services.yaml @@ -13,6 +13,25 @@ send_message: selector: text: +set_log_level: + name: Set Log Level of Add-on + description: Set Log Level of Add-on, this is needed to debug issues happening in eufy-security-ws add-on. Before calling a problematic command, set the log level (preferable to debug), execute your command and revert it back to info. + fields: + log_level: + name: Log Level + description: Log Level Option + required: true + selector: + select: + options: + - silly + - trace + - debug + - info + - warn + - error + - fatal + ptz_360: name: Rotate 360 degrees description: Rotate 360 degrees for supported PTZ cameras @@ -197,6 +216,26 @@ trigger_base_alarm_with_duration: number: min: 0 max: 300 + +chime: + name: Chime on Base Station + description: Only supported if no doorbell device is registered at the station where the chime is to be performed + target: + entity: + domain: alarm_control_panel + integration: eufy_security + fields: + ringtone: + name: Ringtone ID + description: Ringtone ID? + required: true + example: 419 + default: 419 + selector: + number: + min: 0 + max: 999999 + snooze: name: Snooze Alarm description: Snooze Alarm for a Duration