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
20 changes: 13 additions & 7 deletions msmart/device/AC/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class CapabilityId(IntEnum):
FAHRENHEIT = 0x0222
DISPLAY_CONTROL = 0x0224
TEMPERATURES = 0x0225
BUZZER = 0x022C # TODO Reference refers to this as "sound". Is this different then buzzer?
SOUND = 0x022C
MAIN_HORIZONTAL_GUIDE_STRIP = 0x0230 # ??
SUP_HORIZONTAL_GUIDE_STRIP = 0x0231 # ??
TWINS_MACHINE = 0x0232 # ??
Expand All @@ -98,7 +98,7 @@ class PropertyId(IntEnum):
SWING_LR_ANGLE = 0x000A
INDOOR_HUMIDITY = 0x0015 # TODO Reference refers to a potential bug with this
BREEZELESS = 0x0018 # AKA "No Wind Sense"
BUZZER = 0x001A
PROMPT_TONE = 0x001A
SELF_CLEAN = 0x0039
BREEZE_AWAY = 0x0042 # AKA "Prevent Straight Wind"
BREEZE_CONTROL = 0x0043 # AKA "FA No Wind Sense"
Expand All @@ -109,6 +109,7 @@ class PropertyId(IntEnum):
OUT_SILENT = 0x00CD # Portasplit outdoor silent mode
IECO = 0x00E3
ANION = 0x021E
SOUND = 0x022C

@property
def _supported(self) -> bool:
Expand All @@ -117,14 +118,15 @@ def _supported(self) -> bool:
PropertyId.BREEZE_AWAY,
PropertyId.BREEZE_CONTROL,
PropertyId.BREEZELESS,
PropertyId.BUZZER,
PropertyId.CASCADE,
PropertyId.FLASH,
PropertyId.FRESH_AIR,
PropertyId.IECO,
PropertyId.OUT_SILENT,
PropertyId.PROMPT_TONE,
PropertyId.RATE_SELECT,
PropertyId.SELF_CLEAN,
PropertyId.SOUND,
PropertyId.SWING_LR_ANGLE,
PropertyId.SWING_UD_ANGLE,
]
Expand All @@ -134,12 +136,10 @@ def decode(self, data: bytes) -> Any:
if not self._supported:
raise NotImplementedError(f"{repr(self)} decode is not supported.")

if self in [PropertyId.BREEZELESS, PropertyId.FLASH, PropertyId.SELF_CLEAN]:
if self in [PropertyId.BREEZELESS, PropertyId.FLASH, PropertyId.PROMPT_TONE, PropertyId.SELF_CLEAN, PropertyId.SOUND]:
return bool(data[0])
elif self == PropertyId.BREEZE_AWAY:
return data[0] == 2
elif self == PropertyId.BUZZER:
return None # Don't decode buzzer
elif self == PropertyId.CASCADE:
# data[0] - wind_around, data[1] - wind_around_ud
return data[1] if data[0] else 0
Expand All @@ -161,6 +161,8 @@ def encode(self, *args, **kwargs) -> bytes:

if self == PropertyId.BREEZE_AWAY:
return bytes([2 if args[0] else 1])
elif self in [PropertyId.PROMPT_TONE, PropertyId.SOUND]:
return bytes([1 if args[0] else 0])
elif self == PropertyId.CASCADE:
# data[0] - wind_around, data[1] - wind_around_ud
return bytes([1 if args[0] else 0, args[0]])
Expand Down Expand Up @@ -567,7 +569,7 @@ def any_of(w) -> Callable[[memoryview], bool]:
CapabilityId.BREEZE_AWAY: reader("breeze_away", get_value(1)),
CapabilityId.BREEZE_CONTROL: reader("breeze_control", get_value(1)),
CapabilityId.BREEZELESS: reader("breezeless", get_value(1)),
CapabilityId.BUZZER: reader("buzzer", get_value(1)),
CapabilityId.SOUND: reader("sound", get_value(1)),
CapabilityId.CASCADE: reader("cascade", get_value(1)),
CapabilityId.DISPLAY_CONTROL: reader("display_control", any_of([1, 2, 100])),
CapabilityId.ENERGY: [
Expand Down Expand Up @@ -797,6 +799,10 @@ def flash(self) -> bool:
def fresh_air(self) -> bool:
return self._capabilities.get("fresh_air", False)

@property
def sound(self) -> bool:
return self._capabilities.get("sound", False)

@property
def swing_horizontal_angle(self) -> bool:
return self._capabilities.get("swing_horizontal_angle", False)
Expand Down
30 changes: 28 additions & 2 deletions msmart/device/AC/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class Capability(Flag):
OUT_SILENT = auto()
PURIFIER = auto()
SELF_CLEAN = auto()
SOUND = auto()

DEFAULT = (
CUSTOM_FAN_SPEED |
Expand All @@ -164,7 +165,9 @@ class Capability(Flag):
PropertyId.FRESH_AIR: lambda s: s._fresh_air_fan_speed,
PropertyId.IECO: lambda s: (s._ieco_number, s._ieco),
PropertyId.OUT_SILENT: lambda s: s._out_silent,
PropertyId.PROMPT_TONE: lambda s: s._beep_on,
PropertyId.RATE_SELECT: lambda s: s._rate_select,
PropertyId.SOUND: lambda s: s._sound,
PropertyId.SWING_LR_ANGLE: lambda s: s._horizontal_swing_angle,
PropertyId.SWING_UD_ANGLE: lambda s: s._vertical_swing_angle,
}
Expand Down Expand Up @@ -201,6 +204,7 @@ def __init__(self, ip: str, device_id: int, port: int, **kwargs) -> None:

self._fahrenheit_unit = False # Display temperature in Fahrenheit
self._display_on = False
self._sound = None

# Advanced controls
self._follow_me = False
Expand Down Expand Up @@ -403,6 +407,9 @@ def _update_state(self, res: Response) -> None:
if (value := res.get_property(PropertyId.OUT_SILENT)) is not None:
self._out_silent = value

if (value := res.get_property(PropertyId.SOUND)) is not None:
self._sound = value

elif isinstance(res, Group1Response):
_LOGGER.debug("Group 1 response payload from device %s: %s",
self.id, res)
Expand Down Expand Up @@ -593,6 +600,9 @@ def _update_capabilities(self, res: CapabilitiesResponse) -> None:
self._capabilities.set(
AirConditioner.Capability.OUT_SILENT, res.out_silent)

self._capabilities.set(
AirConditioner.Capability.SOUND, res.sound)

# Update supported properties from capabilities
self._update_supported_properties()

Expand All @@ -609,6 +619,7 @@ def _update_supported_properties(self) -> None:
AirConditioner.Capability.IECO: PropertyId.IECO,
AirConditioner.Capability.OUT_SILENT: PropertyId.OUT_SILENT,
AirConditioner.Capability.SELF_CLEAN: PropertyId.SELF_CLEAN,
AirConditioner.Capability.SOUND: PropertyId.SOUND,
AirConditioner.Capability.SWING_HORIZONTAL_ANGLE: PropertyId.SWING_LR_ANGLE,
AirConditioner.Capability.SWING_VERTICAL_ANGLE: PropertyId.SWING_UD_ANGLE,
}
Expand All @@ -625,6 +636,10 @@ def _update_supported_properties(self) -> None:
if self._supported_rate_selects != [AirConditioner.RateSelect.OFF]:
self._supported_properties.add(PropertyId.RATE_SELECT)

# Always support prompt tone if the device supports any properties
if len(self._supported_properties) > 0:
self._supported_properties.add(PropertyId.PROMPT_TONE)

async def _send_commands_get_responses(self, commands: Union[Command, list[Command]]) -> list[Response]:
"""Send a list of commands and return all valid responses."""

Expand Down Expand Up @@ -776,8 +791,8 @@ async def _apply_properties(self, properties: dict[PropertyId, Union[int, bool]]
_LOGGER.warning(
"Device %s is not capable of property %r.", self.id, prop)

# Always add buzzer property
Comment thread
its-tom marked this conversation as resolved.
properties[PropertyId.BUZZER] = self._beep_on
# Always add prompt tone property
properties[PropertyId.PROMPT_TONE] = self._beep_on

# Build command with properties
cmd = SetPropertiesCommand(properties)
Expand Down Expand Up @@ -876,6 +891,16 @@ def beep(self) -> bool:
@beep.setter
def beep(self, tone: bool) -> None:
self._beep_on = tone
self._updated_properties.add(PropertyId.PROMPT_TONE)

@property
def sound(self) -> Optional[bool]:
return self._sound

@sound.setter
def sound(self, enabled: bool) -> None:
self._sound = enabled
self._updated_properties.add(PropertyId.SOUND)

@property
def power_state(self) -> Optional[bool]:
Expand Down Expand Up @@ -1396,6 +1421,7 @@ def to_dict(self) -> dict:
"sleep": self.sleep,
"display_on": self.display_on,
"beep": self.beep,
"sound": self.sound,
"fahrenheit": self.fahrenheit,
"filter_alert": self.filter_alert,
"follow_me": self.follow_me,
Expand Down
17 changes: 13 additions & 4 deletions msmart/device/AC/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,12 @@ def test_encode(self) -> None:
(PropertyId.BREEZE_CONTROL, 0x04): bytes([0x04]),
(PropertyId.BREEZE_CONTROL, 0x00): bytes([0x00]),

# Prompt Tone/Sound: 0x01 - On, 0x00 - Off
(PropertyId.PROMPT_TONE, True): bytes([0x01]),
(PropertyId.PROMPT_TONE, False): bytes([0x00]),
(PropertyId.SOUND, True): bytes([0x01]),
(PropertyId.SOUND, False): bytes([0x00]),

# IECO: 13 bytes ieco_frame, ieco_number, ieco_switch, ...
(PropertyId.IECO, (1, True)): bytes([0, 1, 1]) + bytes(10),
(PropertyId.IECO, (1, False)): bytes([0, 1, 0]) + bytes(10),
Expand Down Expand Up @@ -887,8 +893,11 @@ def test_decode(self) -> None:
(PropertyId.BREEZE_CONTROL, bytes([0x04])): 0x04,
(PropertyId.BREEZE_CONTROL, bytes([0x00])): 0x00,

# Buzzer: Don't decode
(PropertyId.BUZZER, bytes([0x00])): None,
# Prompt Tone/Sound: 0x01 - On, 0x00 - Off
(PropertyId.PROMPT_TONE, bytes([0x01])): True,
(PropertyId.PROMPT_TONE, bytes([0x00])): False,
(PropertyId.SOUND, bytes([0x01])): True,
(PropertyId.SOUND, bytes([0x00])): False,

# IECO: 2 bytes
(PropertyId.IECO, bytes([0x00, 0x00])): False,
Expand Down Expand Up @@ -1000,8 +1009,8 @@ def test_properties_unknown_and_invalid(self) -> None:
# Assert response is a correct type
self.assertEqual(type(resp), PropertiesResponse)

# Assert that the buzzer property is not decoded
self.assertIsNone(resp.get_property(PropertyId.BUZZER))
# Assert that the prompt tone property is decoded correctly
self.assertEqual(resp.get_property(PropertyId.PROMPT_TONE), False)

def test_properties_execution_failed(self) -> None:
"""Test we error when decoding properties that had an execution error."""
Expand Down
Loading