Skip to content

Commit

Permalink
Add initial support for dock firmware and sending mapped buton commands
Browse files Browse the repository at this point in the history
  • Loading branch information
JackJPowell committed Feb 9, 2025
1 parent bea5116 commit 21ad507
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 26 deletions.
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,57 @@ If you are unsure of the password you set, you can change it via the web configu
## External Entity Management
***This is currently in Beta***

Home Assistant now has the ability to manage the entities it shares with your Unfolded Circle Remote. When setting up a new device or when reconfiguring an existing device, you will be taken through an optional step to configure which Home Assistant entities are available on the remote. This functionality mirrors the same options on the integrations page on your remote.
Home Assistant now has the ability to manage the entities it shares with your Unfolded Circle Remote. When setting up a new device or when reconfiguring an existing device, you will be taken through an optional step to configure which Home Assistant entities are available on the remote. This functionality mirrors the same options on the integrations page on your remote.

To get started, add a new device or click the configure button. See the video below for a quick demo.
To get started, add a new device or click the configure button. See the video below for a quick demo.
- You must be running v2.0.0 or greater on your unfolded circle remote for this functionality to be available.
- v2.0+ is currently in beta (But it's very stable)
- This release should work fine for anyone not running the remote beta, but it has only been lightly tested.
- It will not contain any new functionality

https://github.com/user-attachments/assets/96fa94e8-a5ad-4833-9a49-0bf85373eae0

## IR Remote Commands
## Mapped Button Remote Commands
***This is currently in Beta***
The remote entity supports sending predefined IR commands using the unfoldedcircle.send_ir_command action.

For your running activity, you can now send your mapped button commands from within Home Assistant. Using the
remote.send_command or unfoldedcircle.send_button_command action, just specify the `button` from the list below and any additional options.

**num_repeats** (Optional) The number of times to repeat sending the command.
**delay_secs** (Optional) The number of seconds to wait in between sending commands.
**hold** (Optional) Trigger a long press of the supplied button
**activity** (Optional) Identify which activity the button is mapped under. This is only needed if multiple activities are running.

- BACK
- HOME
- VOICE
- VOLUME_UP
- VOLUME_DOWN
- GREEN
- DPAD_UP
- YELLOW
- DPAD_LEFT
- DPAD_MIDDLE
- DPAD_RIGHT
- RED
- DPAD_DOWN
- BLUE
- CHANNEL_UP
- CHANNEL_DOWN
- MUTE
- PREV
- PLAY
- PAUSE
- NEXT
- POWER

## IR Remote Commands

The remote entity supports sending predefined IR commands using the unfoldedcircle.send_ir_command action.

**device:** will match the case-sensitive name of your remote defined in the web configurator on the remote page. This will be your custom name or the manufacturer name selected.

**codeset** (Optional) If you supplied a manufacturer name, you also need to supply the codeset name you are using.
**codeset** (Optional) If you supplied a manufacturer name, you also need to supply the codeset name you are using.

**command** will match the case-senstitive name of the pre-defined (custom or codeset) command defined for that remote.

Expand All @@ -167,7 +201,7 @@ target:
```

> [!TIP]
You can still use the standard remote.send_command action, however, only custom defined remote codes can be sent due to a limitation with this action.
You can still use the standard remote.send_command action, however, only custom defined remote codes can be sent due to a limitation with this action.

## Additional Actions

Expand Down Expand Up @@ -235,16 +269,16 @@ Your Remote Two will now be automatically discovered on the network.

## Wake on lan

Wake on lan support is now available for remotes running firmware version 2.0.0 or higher. Once your remote has been upgraded, and you've turned the feature on, anytime you take a direct action within home assistant to communicate with the remote, it will first attempt to wake the remote up.
Wake on lan support is now available for remotes running firmware version 2.0.0 or higher. Once your remote has been upgraded, and you've turned the feature on, anytime you take a direct action within home assistant to communicate with the remote, it will first attempt to wake the remote up.

## Future Ideas

- [X] Wake on lan was added by the remote developers and has been implemented
- [X] Wake on lan was added by the remote developers and has been implemented

## Notes

- The remote entity does not need to be "on" for it to send commands.
- The Remote Two will go to sleep when unpowered. If you have wake on lan enabled on your remote, Home Assistant will attempt to wake your remote prior to issuing a command. Only commands initiated by you will attempt to wake the remote.
- The Remote Two will go to sleep when unpowered. If you have wake on lan enabled on your remote, Home Assistant will attempt to wake your remote prior to issuing a command. Only commands initiated by you will attempt to wake the remote.
- The remote can now generate its own diagnostic data to aid in debugging via the overflow menu in the Device Info section
- The integration supports multiple Languages: English, French
- The integration will now identify a repair and prompt for a new PIN if it can no longer authenticate to the remote
Expand Down
2 changes: 1 addition & 1 deletion custom_components/unfoldedcircle/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from voluptuous import Optional, Required

from homeassistant import config_entries
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant, callback
Expand Down
1 change: 1 addition & 0 deletions custom_components/unfoldedcircle/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
UPDATE_ACTIVITY_SERVICE = "update_activity"
LEARN_IR_COMMAND_SERVICE = "learn_ir_command"
SEND_IR_COMMAND_SERVICE = "send_ir_command"
SEND_BUTTON_COMMAND_SERVICE = "send_button_command"
HA_SUPPORTED_DOMAINS = [
"binary_sensor",
"button",
Expand Down
2 changes: 1 addition & 1 deletion custom_components/unfoldedcircle/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def should_poll(self) -> bool:
class UnfoldedCircleDockEntity(CoordinatorEntity[UnfoldedCircleDockCoordinator]):
"""Common entity class for all Unfolded Circle Dock entities"""

def __init__(self, coordinator) -> None:
def __init__(self, coordinator: UnfoldedCircleDockCoordinator) -> None:
"""Initialize Unfolded Circle Sensor."""
super().__init__(coordinator)
self.coordinator = coordinator
Expand Down
2 changes: 1 addition & 1 deletion custom_components/unfoldedcircle/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)

from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, RefreshToken
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import NoURLAvailableError, get_url

Expand Down
3 changes: 2 additions & 1 deletion custom_components/unfoldedcircle/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"services": {
"update_activity": "mdi:movie-open-edit-outline",
"learn_ir_command": "mdi:remote",
"send_ir_command": "mdi:remote"
"send_ir_command": "mdi:remote",
"send_button_command": "mdi:remote"
}
}
2 changes: 1 addition & 1 deletion custom_components/unfoldedcircle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"issue_tracker": "https://github.com/JackJPowell/hass-unfoldedcircle/issues",
"requirements": [
"websockets>=12.0,<14",
"pyUnfoldedCircleRemote==0.12.6",
"pyUnfoldedCircleRemote==0.13.1",
"wakeonlan==3.1.0"
],
"version": "0.14.0",
Expand Down
31 changes: 31 additions & 0 deletions custom_components/unfoldedcircle/pyUnfoldedCircleRemote/dock.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ def __init__(
self.apikey = apikey
self._remote_configuration_url = remote_configuration_url
self.websocket = ""
self._update_in_progress = False
self._update_percent = 0

@property
def name(self):
Expand Down Expand Up @@ -273,6 +275,16 @@ def latest_software_version(self):
"""latest_software_version of the dock."""
return self._latest_software_version

@property
def update_in_progress(self):
"""update_in_progress of the dock."""
return self._update_in_progress

@property
def update_percent(self):
"""update_percent of the dock."""
return self._update_percent

@property
def release_notes_url(self):
"""release_notes_url of the dock."""
Expand Down Expand Up @@ -412,6 +424,22 @@ async def get_update_status(self) -> str:

return information

async def update_firmware(self) -> str:
"""Start dock firmware update"""
information = {}
async with (
self.client() as session,
session.post(self.url(f"docks/devices/{self.id}/update")) as response,
):
if response.ok:
information = await response.json()
self._update_in_progress = True
if response.status == 409:
information = {"state": "DOWNLOADING"}
if response.status == 503:
information = {"state": "NO_BATTERY"}
return information

async def start_ir_learning(self) -> str:
"""Start an IR Learning Session"""
async with (
Expand Down Expand Up @@ -574,6 +602,9 @@ def update_from_message(self, message: any) -> None:
_LOGGER.debug("auth is required")
if data["msg"] == "ir_receive":
self._learned_code = data.get("ir_code")
if data["msg"] == "dock_update_change":
self._update_percent = data.get("progress")
self._update_in_progress = True
except Exception:
pass

Expand Down
105 changes: 105 additions & 0 deletions custom_components/unfoldedcircle/pyUnfoldedCircleRemote/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ def __init__(self) -> None:
super().__init__(self.message)


class NoActivityRunning(Exception):
"""Raised when no activities are active."""

def __init__(self) -> None:
"""Raise when no activities are active"""
self.message = "No Activities are currently running"
super().__init__(self.message)


class InvalidButtonCommand(Exception):
"""Raised when an invalid button command is supplied."""

def __init__(self, message) -> None:
"""Raise command no found error."""
self.message = message
super().__init__(self.message)


class EntityCommandError(Exception):
"""Raised when an invalid entity command is supplied."""

def __init__(self, message) -> None:
"""Raise command no found error."""
self.message = message
super().__init__(self.message)


class ExternalSystemNotRegistered(Exception):
"""Raised when an unregistered external system is supplied."""

Expand Down Expand Up @@ -1701,6 +1728,84 @@ async def get_ir_emitters(self) -> list:
self._ir_emitters.append(dock_data.copy())
return self._ir_emitters

async def send_button_command(self, command="", repeat=0, **kwargs) -> bool:
"""Send a predefined button command to the remote kwargs: activity, hold."""
activity_id = None
hold = kwargs.get("hold", False)
activity = kwargs.get("activity", None)
delay_secs = kwargs.get("delay_secs", 0)
repeat = kwargs.get("repeat", 1)

if activity:
for act in self.activities:
if act.name == activity:
activity_id = act.id
else:
for act in self.activities:
if act.is_on():
activity_id = act.id
continue

if not activity_id:
raise NoActivityRunning

try:
entity_id, cmd_id, params = await self.get_physical_button_mapping(
activity_id, command.upper(), hold
)
except HTTPError as ex:
raise InvalidButtonCommand(ex.message) from ex

if self._wake_if_asleep and self._wake_on_lan:
if not await self.wake():
raise RemoteIsSleeping

try:
for _ in range(repeat):
if delay_secs and delay_secs > 0:
await asyncio.sleep(delay_secs)
success = await self.execute_entity_command(entity_id, cmd_id, params)
if not success:
return False
except HTTPError as ex:
raise EntityCommandError(ex.message) from ex

async def get_physical_button_mapping(self, activity_id, button_id, hold) -> str:
"""Get the physical button mapping for the given activity."""
async with (
self.client() as session,
session.get(
self.url(f"activities/{activity_id}/buttons/{button_id}")
) as response,
):
await self.raise_on_error(response)
response = await response.json()

if hold:
action = response.get("long_press")
else:
action = response.get("short_press")

entity_id = action.get("entity_id")
cmd_id = action.get("cmd_id")
params = action.get("params")
return entity_id, cmd_id, params

async def execute_entity_command(self, entity_id, cmd_id, params=None) -> bool:
"""Execute a command on a remote entity."""
body = {"entity_id": entity_id, "cmd_id": cmd_id}
if params:
body["params"] = params
async with (
self.client() as session,
session.put(
self.url("entities/" + entity_id + "/command"),
json=body,
) as response,
):
await self.raise_on_error(response)
return response.status == 200

async def send_remote_command(
self, device="", command="", repeat=0, codeset="", **kwargs
) -> bool:
Expand Down
Loading

0 comments on commit 21ad507

Please sign in to comment.