Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .lync_clone
Submodule .lync_clone added at 527a4b
58 changes: 48 additions & 10 deletions htd_client/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ async def async_connect(self):
self._buffer = bytearray()
self._zone_data = {}
self._zones_loaded = 0
self._zone_data = {}
self._source_names = {}
self._connection = None
self._disconnected = False

Expand Down Expand Up @@ -330,14 +330,16 @@ def _parse_command(self, zone, cmd, data):
# remove the extra null bytes

elif cmd == HtdCommonCommands.ZONE_NAME_RECEIVE_COMMAND:
name = str(data[0:11].decode().rstrip('\0')).lower()
self._zone_data[zone].name = name

elif cmd == HtdCommonCommands.SOURCE_NAME_RECEIVE_COMMAND:
source = data[11]
name = str(data[0:10].decode().rstrip('\0')).lower()
# self.zone_info[zone]['source_list'][source] = name
# self.source_info[zone][name] = source
name = str(data[0:11].decode(errors="ignore").rstrip('\0')).lower()
if self.has_zone_data(zone):
self._zone_data[zone].name = name

elif cmd == HtdCommonCommands.SOURCE_NAME_RECEIVE_COMMAND or cmd == HtdCommonCommands.ZONE_SOURCE_NAME_RECEIVE_COMMAND_LYNC:
source = data[11] + 1
name = str(data[0:10].decode(errors="ignore").rstrip('\0')).lower()
self._source_names[source] = name
if self.has_zone_data(zone):
self._zone_data[zone].source_name = name
#
# elif cmd == HtdCommonCommands.MP3_ON_RECEIVE_COMMAND:
# self.mp3_status['state'] = 'on'
Expand Down Expand Up @@ -389,7 +391,7 @@ def _parse_zone(self, zone_number: int, zone_data: bytearray) -> ZoneDetail | No
HtdConstants.POWER_STATE_TOGGLE_INDEX
)
zone.mute = htd_client.utils.is_bit_on(state_toggles, HtdConstants.MUTE_STATE_TOGGLE_INDEX)
zone.mode = htd_client.utils.is_bit_on(state_toggles, HtdConstants.MODE_STATE_TOGGLE_INDEX)
zone.dnd = htd_client.utils.is_bit_on(state_toggles, HtdConstants.DND_STATE_TOGGLE_INDEX)

zone.source = zone_data[HtdConstants.SOURCE_ZONE_DATA_INDEX] + HtdConstants.SOURCE_QUERY_OFFSET
zone.volume = volume
Expand Down Expand Up @@ -497,6 +499,10 @@ def get_source_count(self) -> int:
"""
return self._model_info['sources']

def get_source_name(self, source: int) -> str:
"""Get the name of a source if it has been fetched."""
return self._source_names.get(source, f"Source {source}")

def get_zone(self, zone: int):
"""
Query a zone and return `ZoneDetail`
Expand Down Expand Up @@ -593,3 +599,35 @@ async def async_balance_left(self, zone: int):
@abstractmethod
async def async_balance_right(self, zone: int):
pass

@abstractmethod
async def async_set_dnd(self, zone: int, dnd: bool):
pass

@abstractmethod
async def async_set_echo(self, echo: bool):
pass

@abstractmethod
async def async_query_id(self):
pass

@abstractmethod
async def async_query_all_zone_status(self):
pass

@abstractmethod
async def async_query_zone_name(self, zone: int):
pass

@abstractmethod
async def async_query_source_name(self, source: int):
pass

@abstractmethod
async def async_set_zone_name(self, zone: int, name: str):
pass

@abstractmethod
async def async_set_source_name(self, source: int, name: str):
pass
6 changes: 5 additions & 1 deletion htd_client/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class HtdConstants:
# indexes of each state toggle
POWER_STATE_TOGGLE_INDEX = 0
MUTE_STATE_TOGGLE_INDEX = 1
MODE_STATE_TOGGLE_INDEX = 2
DND_STATE_TOGGLE_INDEX = 2

# the byte index for where to locate the corresponding setting
SOURCE_ZONE_DATA_INDEX = 4
Expand Down Expand Up @@ -134,6 +134,7 @@ class HtdCommonCommands:
MP3_ARTIST_NAME_RECEIVE_COMMAND = 0x12
MP3_ON_RECEIVE_COMMAND = 0x13
MP3_OFF_RECEIVE_COMMAND = 0x14
QUERY_ID_CODE_RECEIVE_COMMAND = 0x08
ERROR_RECEIVE_COMMAND = 0x1b

EXPECTED_MESSAGE_LENGTH_MAP = {
Expand All @@ -150,6 +151,7 @@ class HtdCommonCommands:
MP3_ON_RECEIVE_COMMAND: 1,
MP3_OFF_RECEIVE_COMMAND: 17,
ERROR_RECEIVE_COMMAND: 9,
QUERY_ID_CODE_RECEIVE_COMMAND: 1,
}

class HtdMcaConstants:
Expand All @@ -171,8 +173,10 @@ class HtdLyncCommands:
BALANCE_SETTING_CONTROL_COMMAND_CODE = 0x16
TREBLE_SETTING_CONTROL_COMMAND_CODE = 0x17
BASS_SETTING_CONTROL_COMMAND_CODE = 0x18
SET_ECHO_COMMAND_CODE = 0x19
SET_AUDIO_TO_DEFAULT_COMMAND_CODE = 0x1c
SET_NAME_TO_DEFAULT_COMMAND_CODE = 0x1e
QUERY_ID_CODE = 0x08

MP3_FAST_FORWARD_COMMAND_CODE = 0x0a
MP3_PLAY_PAUSE_COMMAND_CODE = 0x0b
Expand Down
172 changes: 76 additions & 96 deletions htd_client/lync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,99 +386,79 @@ async def async_set_balance(self, zone: int, balance: int):
balance
)

# def query_zone_name(self, zone: int) -> str:
# """
# Query a zone and return `ZoneDetail`
#
# Args:
# zone (int): the zone
#
# Returns:
# ZoneDetail: a ZoneDetail instance representing the zone requested
#
# Raises:
# Exception: zone X is invalid
# """
#
# # htd_client.utils.validate_zone(zo+ne)
#
# self._send_and_validate(
# zone,
# HtdLyncCommands.QUERY_ZONE_NAME_COMMAND_CODE,
# 0
# )

# def query_source_name(self, source: int, zone: int) -> str:
# source_offset = source - 1
#
# self._send_and_validate(
# zone, HtdLyncCommands.QUERY_SOURCE_NAME_COMMAND_CODE, source_offset
# )
#
# source_name_bytes = response[4:14].strip(b'\x00')
# source_name = htd_client.utils.decode_response(source_name_bytes)
#
# return source_name

# def set_source_name(self, source: int, zone: int, name: str):
# """
# Query a zone and return `ZoneDetail`
#
# Args:
# source (int): the source
# zone: (int): the zone
# name (str): the name of the source (max length of 7)
#
# Returns:
# bytes: a ZoneDetail instance representing the zone requested
#
# Raises:
# Exception: zone X is invalid
# """
#
# # htd_client.utils.validate_zone(zone)
#
# extra_data = bytes(
# [ord(char) for char in name] + [0] * (11 - len(name))
# )
#
# self._send_and_validate(
# zone,
# HtdLyncCommands.SET_SOURCE_NAME_COMMAND_CODE,
# source)
# extra_data
# )
#
# def get_zone_names(self):
# self._send_cmd(
# 1,
# HtdLyncCommands.QUERY_ZONE_NAME_COMMAND_CODE,
# 1
# )
# def set_zone_name(self, zone: int, name: str):
# """
# Query a zone and return `ZoneDetail`
#
# Args:
# zone: (int): the zone
# name (str): the name of the source (max length of 7)
#
# Returns:
# bytes: a ZoneDetail instance representing the zone requested
#
# Raises:
# Exception: zone X is invalid
# """
#
# # htd_client.utils.validate_zone(zone)
#
# extra_data = bytes(
# [ord(char) for char in name] + [0] * (11 - len(name))
# )
#
# self._send_and_validate(
# zone,
# HtdLyncCommands.SET_ZONE_NAME_COMMAND_CODE,
# 0)
# extra_data
# )
async def async_query_all_zone_status(self):
"""Query status of all zones"""
return await self._send_cmd(
0,
HtdLyncCommands.QUERY_COMMAND_CODE,
0
)

async def async_set_dnd(self, zone: int, dnd: bool):
"""Set Do Not Disturb state on/off."""
return await self._async_send_and_validate(
lambda z: z.dnd == dnd,
zone,
HtdLyncCommands.COMMON_COMMAND_CODE,
HtdLyncCommands.DND_ON_COMMAND_CODE if dnd else HtdLyncCommands.DND_OFF_COMMAND_CODE
)

async def async_set_echo(self, echo: bool):
"""Set whether the unit should echo commands back."""
return await self._send_cmd(
0,
HtdLyncCommands.SET_ECHO_COMMAND_CODE if hasattr(HtdLyncCommands, "SET_ECHO_COMMAND_CODE") else 0x19,
0xFF if echo else 0x00
)

async def async_query_id(self):
"""Query device ID."""
return await self._send_cmd(
0,
HtdLyncCommands.QUERY_ID_CODE if hasattr(HtdLyncCommands, "QUERY_ID_CODE") else 0x08,
0x00
)

async def async_query_zone_name(self, zone: int):
"""Query the name of a zone."""
await self._send_cmd(
zone,
HtdLyncCommands.QUERY_ZONE_NAME_COMMAND_CODE,
0
)

async def async_query_source_name(self, source: int):
"""Query the name of a source."""
await self._send_cmd(
1,
HtdLyncCommands.QUERY_SOURCE_NAME_COMMAND_CODE,
source
)

async def async_set_source_name(self, source: int, name: str):
"""Set the name of a source (max 10 chars)."""
trimmed = name[:10]
encoded = list(trimmed.encode("ascii", errors="ignore"))
encoded.extend([0] * (10 - len(encoded)))
extra_data = bytearray(encoded + [0])

await self._send_cmd(
0,
HtdLyncCommands.SET_SOURCE_NAME_COMMAND_CODE,
source,
extra_data
)

async def async_set_zone_name(self, zone: int, name: str):
"""Set the name of a zone (max 10 chars)."""
trimmed = name[:10]
encoded = list(trimmed.encode("ascii", errors="ignore"))
encoded.extend([0] * (10 - len(encoded)))
extra_data = bytearray(encoded + [0])

await self._send_cmd(
zone,
HtdLyncCommands.SET_ZONE_NAME_COMMAND_CODE,
0,
extra_data
)
82 changes: 37 additions & 45 deletions htd_client/mca_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,48 +427,40 @@ async def async_balance_right(self, zone: int):
HtdMcaCommands.BALANCE_RIGHT_COMMAND
)

# def get_source_names(self):
# """
# Query a zone and return `ZoneDetail`
#
# Returns:
# Dict[int, str]: a dictionary where each zone has a string value
# of the source name
# """
#
# self._send_cmd(
# 0,
# HtdMcaCommands.QUERY_SOURCE_NAME_COMMAND_CODE,
# 0
# )
#
# def set_source_name(self, source: int, name: str):
# """
# Query a zone and return `ZoneDetail`
#
# Args:
# source (int): the source
# name (str): the name of the source (max length of 7)
#
# Returns:
# ZoneDetail: a ZoneDetail instance representing the zone requested
#
# Raises:
# Exception: zone X is invalid
# """
#
# # htd_client.utils.validate_zone(zone)
#
# extra_data = bytearray(
# [ord(char) for char in name] + [0] * (7 - len(name)) + [0x00]
# )
#
# self._send_cmd(
# 0,
# HtdMcaCommands.SET_SOURCE_NAME_COMMAND_CODE,
# source,
# extra_data
# )
#
# def get_zone_names(self):
# pass
async def async_set_dnd(self, zone: int, dnd: bool):
raise NotImplementedError("MCA does not support DND.")

async def async_set_echo(self, echo: bool):
raise NotImplementedError("MCA does not support echo setting.")

async def async_query_id(self):
raise NotImplementedError("MCA does not support ID query.")

async def async_query_all_zone_status(self):
raise NotImplementedError("MCA does not support global status query.")

async def async_query_zone_name(self, zone: int):
raise NotImplementedError("MCA does not support querying zone names.")

async def async_set_zone_name(self, zone: int, name: str):
raise NotImplementedError("MCA does not support setting zone names.")

async def async_query_source_name(self, source: int):
"""Query a source name."""
await self._send_cmd(
0,
HtdMcaCommands.QUERY_SOURCE_NAME_COMMAND_CODE,
0
)

async def async_set_source_name(self, source: int, name: str):
"""Set the name of a source (max 7 chars)."""
extra_data = bytearray(
[ord(char) for char in name[:7]] + [0] * (7 - len(name[:7])) + [0x00]
)
await self._send_cmd(
0,
HtdMcaCommands.SET_SOURCE_NAME_COMMAND_CODE,
source,
extra_data
)
Loading