Skip to content

Commit

Permalink
feat: ringtone service and v5.3.1
Browse files Browse the repository at this point in the history
  • Loading branch information
fuatakgun committed Dec 30, 2022
1 parent eb2f8bf commit d065f4b
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 41 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions custom_components/eufy_security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions custom_components/eufy_security/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions custom_components/eufy_security/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 0 additions & 3 deletions custom_components/eufy_security/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
84 changes: 49 additions & 35 deletions custom_components/eufy_security/eufy_security_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/eufy_security/eufy_security_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,6 +58,7 @@ class MessageField(Enum):
CURRENT_MODE = "currentMode"
GUARD_MODE = "guardMode"
SECONDS = "seconds"
RINGTONE = "ringtone"

# camera specific
PICTURE_URL = "pictureUrl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ class OutgoingMessageToParameter(Enum):
snoozeChime = "snooze_chime"
snoozeMotion = "snooze_motion"
snoozeHomebase = "snooze_homebase"
level = "log_level"
ringtone = "ringtone"


class OutgoingMessageType(Enum):
"""Outgoing message types"""

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}
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions custom_components/eufy_security/eufy_security_api/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion custom_components/eufy_security/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions custom_components/eufy_security/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit d065f4b

Please sign in to comment.