Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for device type 0xC3 (heat pumps) #108

Draft
wants to merge 69 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
213a3fa
Reorganize references in prep for C3 device
mill1000 Jan 9, 2024
c290d84
Add 0xC3 device references
mill1000 Jan 9, 2024
7bbc4c7
Add heat pump to device type enum
mill1000 Jan 9, 2024
99fad71
Add some basic types from the reference code
mill1000 Jan 9, 2024
d836656
Add skeleton device class for Heat pump
mill1000 Jan 10, 2024
f72ee67
Add some basic command classes for querying device state
mill1000 Jan 10, 2024
cf57532
Add basic control command
mill1000 Jan 10, 2024
0476268
Add some skeleton response classes
mill1000 Jan 12, 2024
1bdbdb0
Use bytearray for payload
mill1000 Jan 12, 2024
492c52c
Simplify CRC8 implementation
mill1000 Jan 12, 2024
28dff33
Overhaul base command class for C3 device support
mill1000 Jan 12, 2024
1243dbc
Remove ABC from class inheritance. Likely wasn't being used right
mill1000 Jan 12, 2024
204050f
Move Frame checksum validation into class
mill1000 Jan 12, 2024
b32ecf5
Flesh out enough response handling to start testing message RX
mill1000 Jan 12, 2024
37f0b97
Add heatpump to device __init__
mill1000 Jan 12, 2024
800e159
Rename base_command to frame
mill1000 Jan 12, 2024
66b038c
Fix typing for Python 3.8
mill1000 Jan 13, 2024
0a14f5c
Fix Heat pump device type. It's 0xC3 not 0x3C...
mill1000 Jan 13, 2024
e0edf5c
Remove extra byte of frame length, apparently this differs between 0x…
mill1000 Feb 4, 2024
cbea092
Fix ranges on payload which truncated packet to 0
mill1000 Feb 4, 2024
55a700c
Adjust length check on QueryBasicResponse payload
mill1000 Feb 4, 2024
8566e65
Add basic testcase for QueryBasicResponse
mill1000 Feb 4, 2024
aa3407f
Autopep fix
mill1000 Feb 4, 2024
6efaaa6
Convert temperature types to ints
mill1000 Feb 4, 2024
26b622f
Handle invalid tank temperature
mill1000 Feb 4, 2024
1586cc1
Update some todos in QueryBasicResponse
mill1000 Feb 4, 2024
667717d
Fix zone2 terminal type parsing
mill1000 Feb 4, 2024
e971a4b
Decode room thermostat bits
mill1000 Feb 5, 2024
8507a26
Start fleshing out the heat pump device
mill1000 Feb 5, 2024
14ab397
Update some basic query response names and comment the original refer…
mill1000 Feb 5, 2024
38197ae
Apply isort
mill1000 Feb 6, 2024
e443b40
Add additional enums
mill1000 Feb 6, 2024
e2ea86c
Add some additional comments to basic query response
mill1000 Feb 6, 2024
4083b28
Tweak ControlBasicCommand
mill1000 Feb 6, 2024
be9bbdf
Adjust some command property names
mill1000 Feb 6, 2024
89df266
Flesh out more device properties and add a refresh function to get th…
mill1000 Feb 6, 2024
a165617
Add command and response for Unit Parameters which contains some usef…
mill1000 Feb 6, 2024
9f93982
Fix silly typo in update_state
mill1000 Feb 7, 2024
1846a2e
Add minimal testcase for device state updates
mill1000 Feb 7, 2024
70384aa
Parse Unit Parameters responses
mill1000 Feb 7, 2024
342e35b
Query and process unit parameters
mill1000 Feb 7, 2024
6dd49a2
Add HMI query type
mill1000 Feb 9, 2024
e10645d
Minor tweaks to zone parsing
mill1000 Feb 13, 2024
4c48e82
Add properties for DHW
mill1000 Feb 13, 2024
acc9e82
Disable UnitParameters query
mill1000 Feb 16, 2024
2469f83
Rename FrameType members
mill1000 Mar 3, 2024
35bf2e4
Add support for POWER4 report "responses"
mill1000 Mar 3, 2024
f4a5352
Check expected attributes in response tests
mill1000 Mar 3, 2024
9283431
Don't use "temp" shorthand
mill1000 Mar 3, 2024
33b3263
Replace unit parameter parsing with power4 report parsing
mill1000 Mar 6, 2024
2a498f5
Add properties to device class and zone classes
mill1000 Mar 6, 2024
dc2ab8e
Define device apply method and send ControlBasic commands
mill1000 Mar 6, 2024
8eea279
Update defaults of ControlBasic
mill1000 Mar 6, 2024
3c3b453
Fix typing violations
mill1000 Mar 6, 2024
497789a
Clean up pylint violations
mill1000 Mar 6, 2024
64070a5
Add run_mode properties
mill1000 Mar 6, 2024
bea1236
Add to_dict to print device state
mill1000 Mar 6, 2024
74dbd58
Add DHW power state properties
mill1000 Mar 6, 2024
02d923b
Fix bad reference to _temp_type
mill1000 Mar 6, 2024
5ecd890
FIx out of date FrameType references due to rebase
mill1000 Mar 6, 2024
07c124c
Fix incorrect frame type on control commands and add basic testcase f…
mill1000 Mar 18, 2024
44c02e7
Fix parsing of zone2 terminal type
mill1000 Mar 18, 2024
414c356
Process responses from control basic commands as query basic responses
mill1000 Mar 18, 2024
98ee626
Extract TBH enable from basic query response
mill1000 Mar 18, 2024
232e82e
Remove underscores on zone members
mill1000 Mar 18, 2024
bc960d3
Rename fastdhw_state to dhw_fast_mode
mill1000 Mar 18, 2024
97a9f29
Expose TBH and DHW Fast mode as device properties
mill1000 Mar 18, 2024
772fc06
Add TBH and fast DHW mode to to_dict
mill1000 Mar 18, 2024
e404da8
Fix incorrect attributes names
mill1000 Mar 19, 2024
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
71 changes: 0 additions & 71 deletions msmart/base_command.py

This file was deleted.

4 changes: 2 additions & 2 deletions msmart/base_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import time
from typing import List, Optional

from msmart.base_command import Command
from msmart.const import DeviceType
from msmart.frame import Frame
from msmart.lan import LAN, AuthenticationError, Key, ProtocolError, Token

_LOGGER = logging.getLogger(__name__)
Expand All @@ -25,7 +25,7 @@ def __init__(self, *, ip: str, port: int, device_id: int, device_type: DeviceTyp
self._supported = False
self._online = False

async def _send_command(self, command: Command) -> Optional[List[bytes]]:
async def _send_command(self, command: Frame) -> Optional[List[bytes]]:
"""Send a command to the device and return any responses."""

data = command.tobytes()
Expand Down
5 changes: 3 additions & 2 deletions msmart/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@

class DeviceType(IntEnum):
AIR_CONDITIONER = 0xAC
HEAT_PUMP = 0xC3


class FrameType(IntEnum):
UNKNOWN = 0
SET = 0x02
REQUEST = 0x03
CONTROL = 0x02
QUERY = 0x03
REPORT = 0x04
ABNORMAL_REPORT = 0x06
7 changes: 1 addition & 6 deletions msmart/crc8.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,5 @@
def calculate(data: bytes) -> int:
crc_value = 0
for m in data:
k = crc_value ^ m
if k > 256:
k -= 256
if k < 0:
k += 256
crc_value = _CRC8_854_TABLE[k]
crc_value = _CRC8_854_TABLE[(crc_value ^ m) & 0xFF]
return crc_value
79 changes: 46 additions & 33 deletions msmart/device/AC/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from typing import Callable, Collection, Mapping, Optional, Union

import msmart.crc8 as crc8
from msmart.base_command import Command
from msmart.const import DeviceType, FrameType
from msmart.frame import Frame, InvalidFrameException

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -85,31 +85,47 @@ class TemperatureType(IntEnum):
OUTDOOR = 0x3


class Command(Frame):
"""Base class for AC commands."""

_message_id = 0

def __init__(self, device_type: DeviceType, frame_type: FrameType) -> None:
super().__init__(device_type, frame_type)

def tobytes(self, data: Union[bytes, bytearray] = bytes()) -> bytes:
# Append message ID to payload
# TODO Message ID in reference is just a random value
payload = data + bytes([self._next_message_id()])

# Append CRC
return super().tobytes(payload + bytes([crc8.calculate(payload)]))

def _next_message_id(self) -> int:
Command._message_id += 1
return Command._message_id & 0xFF


class GetCapabilitiesCommand(Command):
def __init__(self, additional: bool = False) -> None:
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.REQUEST)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.QUERY)

self._additional = additional

@property
def payload(self) -> bytes:
if not self._additional:
# Get capabilities
return bytes([0xB5, 0x01, 0x00])
else:
# Get more capabilities
return bytes([0xB5, 0x01, 0x01, 0x1])
def tobytes(self) -> bytes:
paylaod = bytes([0xB5, 0x01, 0x00] if not self._additional
else [0xB5, 0x01, 0x01, 0x1])
return super().tobytes(paylaod)


class GetStateCommand(Command):
def __init__(self) -> None:
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.REQUEST)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.QUERY)

self.temperature_type = TemperatureType.INDOOR

@property
def payload(self) -> bytes:
return bytes([
def tobytes(self) -> bytes:
return super().tobytes(bytes([
# Get state
0x41,
# Unknown
Expand All @@ -122,12 +138,12 @@ def payload(self) -> bytes:
0x00, 0x00, 0x00, 0x00,
# Unknown
0x03,
])
]))


class SetStateCommand(Command):
def __init__(self) -> None:
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.SET)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.CONTROL)

self.beep_on = True
self.power_on = False
Expand All @@ -142,8 +158,7 @@ def __init__(self) -> None:
self.freeze_protection_mode = False
self.follow_me = False

@property
def payload(self) -> bytes:
def tobytes(self) -> bytes:
# Build beep and power status bytes
beep = 0x40 if self.beep_on else 0
power = 0x1 if self.power_on else 0
Expand Down Expand Up @@ -185,7 +200,7 @@ def payload(self) -> bytes:
# Build alternate turbo byte
freeze_protect = 0x80 if self.freeze_protection_mode else 0

return bytes([
return super().tobytes(bytes([
# Set state
0x40,
# Beep and power state
Expand Down Expand Up @@ -215,22 +230,21 @@ def payload(self) -> bytes:
freeze_protect,
# Unknown
0x00, 0x00,
])
]))


class ToggleDisplayCommand(Command):
def __init__(self) -> None:
# For whatever reason, toggle display uses a request type...
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.REQUEST)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.QUERY)

self.beep_on = True

@property
def payload(self) -> bytes:
def tobytes(self) -> bytes:
# Set beep bit
beep = 0x40 if self.beep_on else 0

return bytes([
return super().tobytes(bytes([
# Get state
0x41,
# Beep and other flags
Expand All @@ -241,14 +255,14 @@ def payload(self) -> bytes:
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
])
]))


class GetPropertiesCommand(Command):
"""Command to query specific properties from the device."""

def __init__(self, props: Collection[PropertyId]) -> None:
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.REQUEST)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.QUERY)

self._properties = props

Expand All @@ -269,7 +283,7 @@ class SetPropertiesCommand(Command):
"""Command to set specific properties of the device."""

def __init__(self, props: Mapping[PropertyId, Union[bytes, int]]) -> None:
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.SET)
super().__init__(DeviceType.AIR_CONDITIONER, frame_type=FrameType.CONTROL)

self._properties = props

Expand Down Expand Up @@ -308,18 +322,17 @@ def payload(self) -> bytes:

@classmethod
def validate(cls, frame: memoryview) -> None:
# Validate frame checksum
frame_checksum = Command.checksum(frame[1:-1])
if frame_checksum != frame[-1]:
raise InvalidResponseException(
f"Frame '{frame.hex()}' failed checksum. Received: 0x{frame[-1]:X}, Expected: 0x{frame_checksum:X}.")
try:
Frame.validate(frame)
except InvalidFrameException as e:
raise InvalidResponseException(e) from e

# Extract frame payload to validate CRC/checksum
payload = frame[10:-1]

# Some devices use a CRC others seem to use a 2nd checksum
payload_crc = crc8.calculate(payload[0:-1])
payload_checksum = Command.checksum(payload[0:-1])
payload_checksum = Frame.checksum(payload[0:-1])

if payload_crc != payload[-1] and payload_checksum != payload[-1]:
raise InvalidResponseException(
Expand Down
Empty file added msmart/device/C3/__init__.py
Empty file.
Loading