Skip to content

Commit

Permalink
support relative seek #8
Browse files Browse the repository at this point in the history
expose zone name as an attribute #7
allow options to configure mac addresses
  • Loading branch information
3ll3d00d committed Jan 14, 2024
1 parent ed026e8 commit 735a63a
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 32 deletions.
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,28 @@ Targets the `media_player` entity.

[![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_call_service.svg)](https://my.home-assistant.io/redirect/developer_call_service/?service=jriver.add_to_playlist)

Accepts one of two parameters:
Requires one of two parameters:

* query: a valid search expression
* playlist_path

#### jriver.relative_seek

Targets the `media_player` entity.

[![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_call_service.svg)](https://my.home-assistant.io/redirect/developer_call_service/?service=jriver.seek_relative)

Requires a single parameter:

* seek_duration: an amount to seek by in seconds

#### jriver.activate_zone

Targets the `remote` entity.

[![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_call_service.svg)](https://my.home-assistant.io/redirect/developer_call_service/?service=jriver.activate_zone)

Accepts a single parameter
Requires a single parameter

* zone_name

Expand All @@ -137,13 +147,25 @@ Targets the `remote` entity.

[![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_call_service.svg)](https://my.home-assistant.io/redirect/developer_call_service/?service=jriver.activate_zone)

Exposes MCWS/v1/Control/MCC as a service and accepts the same parameters,i.e.
Exposes MCWS/v1/Control/MCC as a service and accepts the same parameters, i.e.

* command
* parameter
* block
* zone_name

Minimally, command is required.

#### jriver.wake

Targets the `remote` entity.

[![Open your Home Assistant instance and show your service developer tools.](https://my.home-assistant.io/badges/developer_call_service.svg)](https://my.home-assistant.io/redirect/developer_call_service/?service=jriver.wake)

Sends a WOL packet to the configured MAC addresses.

Depends on [Wake on LAN](https://www.home-assistant.io/integrations/wake_on_lan/) being enabled and an access key used to connect to Media Server.

# Dev

Create a virtual environment to install dependencies:
Expand Down
5 changes: 4 additions & 1 deletion custom_components/jriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ async def _close(event):
if CONF_BROWSE_PATHS in entry.options
else entry.data[CONF_BROWSE_PATHS]
)
mac_addresses = (
entry.options[CONF_MAC] if CONF_MAC in entry.options else entry.data[CONF_MAC]
)
hass.data[DOMAIN][entry.entry_id] = {
DATA_MEDIA_SERVER: ms,
DATA_REMOVE_STOP_LISTENER: remove_stop_listener,
Expand All @@ -285,7 +288,7 @@ async def _close(event):
DATA_COORDINATOR: coordinator,
DATA_SERVER_NAME: entry.data.get(CONF_NAME, ms.media_server_info.name),
DATA_EXTRA_FIELDS: extra_fields,
DATA_MAC_ADDRESSES: entry.data[CONF_MAC],
DATA_MAC_ADDRESSES: mac_addresses,
}

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand Down
64 changes: 53 additions & 11 deletions custom_components/jriver/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@
CONF_DEVICE_ZONES,
CONF_EXTRA_FIELDS,
CONF_USE_WOL,
DATA_BROWSE_PATHS,
DATA_EXTRA_FIELDS,
DEFAULT_BROWSE_PATHS,
DEFAULT_DEVICE_PER_ZONE,
DEFAULT_PORT,
Expand Down Expand Up @@ -515,8 +513,10 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self._library_fields: list[str] = []
self._browse_paths: list[str] = []
self._extra_fields: list[str] = []
self._browse_paths: list[str] = self._get_existing(CONF_BROWSE_PATHS, [])
self._extra_fields: list[str] = self._get_existing(CONF_EXTRA_FIELDS, [])
self._mac_addresses: list[str] = self._get_existing(CONF_MAC, [])
self._use_wol: bool = self._get_existing(CONF_USE_WOL, True)

async def async_step_init(
self, user_input: dict[str, Any] | None = None
Expand All @@ -526,7 +526,7 @@ async def async_step_init(
if user_input is not None:
self._browse_paths = user_input.get(CONF_BROWSE_PATHS, [])
if self._browse_paths:
return await self.async_step_fields()
return await self.async_step_macs()
errors["base"] = "no_paths"

return self.async_show_form(
Expand All @@ -535,9 +535,7 @@ async def async_step_init(
{
vol.Required(
CONF_BROWSE_PATHS,
default=self.config_entry.options[DATA_BROWSE_PATHS]
if DATA_BROWSE_PATHS in self.config_entry.options
else self.config_entry.data[DATA_BROWSE_PATHS],
default=self._browse_paths,
): TextSelector(
TextSelectorConfig(
type=TextSelectorType.TEXT, multiline=True, multiple=True
Expand Down Expand Up @@ -589,9 +587,7 @@ async def async_step_fields(
{
vol.Required(
CONF_EXTRA_FIELDS,
default=self.config_entry.options[DATA_EXTRA_FIELDS]
if DATA_EXTRA_FIELDS in self.config_entry.options
else self.config_entry.data[DATA_EXTRA_FIELDS],
default=self._extra_fields,
): SelectSelector(
SelectSelectorConfig(multiple=True, options=self._library_fields)
),
Expand All @@ -600,15 +596,61 @@ async def async_step_fields(

return self.async_show_form(step_id="fields", data_schema=schema, errors=errors)

async def async_step_macs(self, user_input=None):
"""Handle mac address input."""
schema = vol.Schema(
{
vol.Required(CONF_USE_WOL, default=True): bool,
vol.Optional(
CONF_MAC,
default=self._mac_addresses,
): TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT, multiple=True)
),
}
)

errors = {}
if user_input is not None:
macs = user_input[CONF_MAC]
use_wol = user_input[CONF_USE_WOL] is True
if not macs and use_wol:
errors["base"] = "no_mac_addresses"
return self.async_show_form(
step_id="macs", data_schema=schema, errors=errors
)

self._mac_addresses = macs if use_wol else []
if any(_invalid_mac(m) for m in self._mac_addresses):
errors["base"] = "invalid_mac"
return self.async_show_form(
step_id="macs", data_schema=schema, errors=errors
)

self._mac_addresses = [m.replace("-", ":") for m in macs]

return await self.async_step_fields()

return self.async_show_form(step_id="macs", data_schema=schema, errors=errors)

@callback
def _get_data(self):
data = {
CONF_BROWSE_PATHS: self._browse_paths,
CONF_EXTRA_FIELDS: self._extra_fields,
CONF_MAC: self._mac_addresses,
CONF_USE_WOL: self._use_wol,
}

return data

def _get_existing(self, key: str, default_value: Any = None) -> Any:
if key in self.config_entry.options:
return self.config_entry.options[key]
if key in self.config_entry.data:
return self.config_entry.data[key]
return default_value


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/jriver/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"iot_class": "local_polling",
"loggers": ["hamcws"],
"requirements": ["hamcws==0.1.7"],
"version": "0.0.4",
"version": "0.0.5",
"issue_tracker": "https://github.com/3ll3d00d/jriver_homeassistant/issues"
}
24 changes: 23 additions & 1 deletion custom_components/jriver/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@
}


SERVICE_SEEK_RELATIVE = "seek_relative"

ATTR_SEEK_DURATION = "seek_duration"

MC_SEEK_RELATIVE_SCHEMA = {
vol.Required(ATTR_SEEK_DURATION): vol.Coerce(float),
}


def find_matching_config_entries_for_key_value(hass, key, value):
"""Search existing config entries for a match."""
for entry in hass.config_entries.async_entries(DOMAIN):
Expand Down Expand Up @@ -151,6 +160,9 @@ async def async_setup_entry(
platform.async_register_entity_service(
SERVICE_ADD_MEDIA, MC_ADD_MEDIA_SCHEMA, "async_add_media_to_playlist"
)
platform.async_register_entity_service(
SERVICE_SEEK_RELATIVE, MC_SEEK_RELATIVE_SCHEMA, "async_seek_relative"
)

data = hass.data[DOMAIN][config_entry.entry_id]
ms: MediaServer = data[DATA_MEDIA_SERVER]
Expand Down Expand Up @@ -255,7 +267,10 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
if not self._playback_info:
return None

return self._playback_info.extra_fields
return {
"zone_name": self._playback_info.zone_name,
**self._playback_info.extra_fields,
}

@callback
def _handle_coordinator_update(self) -> None:
Expand Down Expand Up @@ -543,6 +558,13 @@ async def async_add_media_to_playlist(
"Service add_to_playlist requires either query or playlist_path to be set"
)

@cmd
async def async_seek_relative(self, seek_duration: float):
"""Seek by the specified duration."""
await self._media_server.media_seek(
int(seek_duration * 1000), zone=self._target_zone
)

async def async_browse_media(
self,
media_content_type: MediaType | str | None = None,
Expand Down
29 changes: 18 additions & 11 deletions custom_components/jriver/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,22 @@ async def async_send_mcc(
@cmd
async def async_send_wol(self):
"""Send WOL packet to each MAC address."""

if self._mac_addresses and self._hass.services.has_service(
WOL_DOMAIN, SERVICE_SEND_MAGIC_PACKET
):
await asyncio.gather(
*[
self._hass.services.async_call(
WOL_DOMAIN, SERVICE_SEND_MAGIC_PACKET, service_data={"mac": mac}
)
for mac in self._mac_addresses
]
if not self._mac_addresses:
_LOGGER.debug("No MAC addresses available, unable to send WOL")
return
if not self._hass.services.has_service(WOL_DOMAIN, SERVICE_SEND_MAGIC_PACKET):
_LOGGER.warning(
"Service wake_on_lan not configured, unable to send WOL to %s",
str(self._mac_addresses),
)
return

_LOGGER.debug("Sending WOL to %s", str(self._mac_addresses))
await asyncio.gather(
*[
self._hass.services.async_call(
WOL_DOMAIN, SERVICE_SEND_MAGIC_PACKET, service_data={"mac": mac}
)
for mac in self._mac_addresses
]
)
6 changes: 5 additions & 1 deletion custom_components/jriver/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,8 @@ def extra_state_attributes(self) -> Mapping[str, Any]:
info = self.coordinator.data.get_playback_info(self._zone_name)
if not info:
return {}
return info.as_dict()
return {
"is_active": self.coordinator.data.get_active_zone_name()
== self._zone_name,
**info.as_dict(),
}
11 changes: 11 additions & 0 deletions custom_components/jriver/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ add_to_playlist:
selector:
text:

seek_relative:
target:
entity:
integration: jriver
domain: media_player
fields:
seek_duration:
example: -5
selector:
number:

activate_zone:
target:
entity:
Expand Down
23 changes: 21 additions & 2 deletions custom_components/jriver/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"no_paths": "No view paths provided",
"invalid_paths": "Path format is incorrect.",
"no_zones": "Must select at least one zone.",
"no_macs": "Must provide at least one MAC address.",
"no_mac_addresses": "Must provide at least one MAC address or disable WOL support.",
"invalid_mac": "Invalid MAC address, must be 6 pairs of hex digits separated by :"
},
"abort": {
Expand All @@ -87,6 +87,13 @@
"data": {
"extra_fields": "Field Name"
}
},
"macs": {
"description": "Select the MAC addresses that should be used for wake on lan.\nRequires the Home Assistant Wake on LAN integration to be enabled.",
"data": {
"use_wol": "Enable remote.wake service.",
"mac": "MAC address"
}
}
},
"error": {
Expand All @@ -96,7 +103,9 @@
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"no_paths": "No view paths provided",
"invalid_paths": "Path format is incorrect."
"invalid_paths": "Path format is incorrect.",
"no_mac_addresses": "Must provide at least one MAC address or disable WOL support.",
"invalid_mac": "Invalid MAC address, must be 6 pairs of hex digits separated by :"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
Expand Down Expand Up @@ -130,6 +139,16 @@
}
}
},
"seek_relative": {
"name": "Relative position seek",
"description": "Move forward or backward by the specified amount in seconds. ",
"fields": {
"seek_duration": {
"name": "Seek amount",
"description": "Number of seconds to seek forwards or backwards."
}
}
},
"send_mcc": {
"name": "Send MCC command",
"description": "Sends an MCC command. ",
Expand Down
Loading

0 comments on commit 735a63a

Please sign in to comment.