diff --git a/aiobafi6/device.py b/aiobafi6/device.py index 039754f..c7a2cfb 100644 --- a/aiobafi6/device.py +++ b/aiobafi6/device.py @@ -464,6 +464,13 @@ def has_fan(self) -> bool: def has_light(self) -> t.Optional[bool]: return maybe_proto_field(self._properties.capabilities, "has_light") + @property + def has_auto_comfort(self) -> bool: + # https://github.com/home-assistant/core/issues/72934 + c1 = maybe_proto_field(self._properties.capabilities, "has_comfort1") or False + c3 = maybe_proto_field(self._properties.capabilities, "has_comfort3") or False + return c1 and c3 + # Fan fan_mode = ProtoProp[OffOnAuto](writable=True, from_proto=lambda v: OffOnAuto(v)) diff --git a/aiobafi6/device_test.py b/aiobafi6/device_test.py index 16d01bb..1d57830 100644 --- a/aiobafi6/device_test.py +++ b/aiobafi6/device_test.py @@ -29,3 +29,32 @@ async def test_service_property_read_only(): d = Device(Service(("127.0.0.1",), PORT)) with pytest.raises(AttributeError): d.service = Service(("127.0.0.2",), PORT) # type: ignore + + +@pytest.mark.asyncio +async def test_has_auto_comfort(): + d = Device(Service(("127.0.0.1",), PORT)) + assert not d.has_auto_comfort + + d._properties.capabilities.has_comfort1 = False + assert not d.has_auto_comfort + + d._properties.capabilities.ClearField("has_comfort1") + d._properties.capabilities.has_comfort3 = False + assert not d.has_auto_comfort + + d._properties.capabilities.has_comfort1 = False + d._properties.capabilities.has_comfort3 = False + assert not d.has_auto_comfort + + d._properties.capabilities.ClearField("has_comfort3") + d._properties.capabilities.has_comfort1 = True + assert not d.has_auto_comfort + + d._properties.capabilities.ClearField("has_comfort1") + d._properties.capabilities.has_comfort3 = True + assert not d.has_auto_comfort + + d._properties.capabilities.has_comfort1 = True + d._properties.capabilities.has_comfort3 = True + assert d.has_auto_comfort diff --git a/aiobafi6/proto/aiobafi6_pb2.py b/aiobafi6/proto/aiobafi6_pb2.py index 82df3f1..5575207 100644 --- a/aiobafi6/proto/aiobafi6_pb2.py +++ b/aiobafi6/proto/aiobafi6_pb2.py @@ -13,17 +13,17 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14proto/aiobafi6.proto\x12\x08\x61iobafi6\"&\n\x04Root\x12\x1e\n\x05root2\x18\x02 \x01(\x0b\x32\x0f.aiobafi6.Root2\"v\n\x05Root2\x12 \n\x06\x63ommit\x18\x02 \x01(\x0b\x32\x10.aiobafi6.Commit\x12\x1e\n\x05query\x18\x03 \x01(\x0b\x32\x0f.aiobafi6.Query\x12+\n\x0cquery_result\x18\x04 \x01(\x0b\x32\x15.aiobafi6.QueryResult\"2\n\x06\x43ommit\x12(\n\nproperties\x18\x03 \x01(\x0b\x32\x14.aiobafi6.Properties\"7\n\x05Query\x12.\n\x0eproperty_query\x18\x01 \x01(\x0e\x32\x16.aiobafi6.ProperyQuery\"^\n\x0bQueryResult\x12(\n\nproperties\x18\x02 \x03(\x0b\x32\x14.aiobafi6.Properties\x12%\n\tschedules\x18\x03 \x03(\x0b\x32\x12.aiobafi6.Schedule\"\xd5\x0b\n\nProperties\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x16\n\x0elocal_datetime\x18\x04 \x01(\t\x12\x14\n\x0cutc_datetime\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x07 \x01(\t\x12\x13\n\x0bmac_address\x18\x08 \x01(\t\x12\r\n\x05uuid9\x18\t \x01(\t\x12\x13\n\x0b\x64ns_sd_uuid\x18\n \x01(\t\x12\x14\n\x0c\x61pi_endpoint\x18\x0b \x01(\t\x12\x13\n\x0b\x61pi_version\x18\r \x01(\t\x12.\n\x08\x66irmware\x18\x10 \x01(\x0b\x32\x1c.aiobafi6.FirmwareProperties\x12,\n\x0c\x63\x61pabilities\x18\x11 \x01(\x0b\x32\x16.aiobafi6.Capabilities\x12%\n\x08\x66\x61n_mode\x18+ \x01(\x0e\x32\x13.aiobafi6.OffOnAuto\x12\x16\n\x0ereverse_enable\x18, \x01(\x08\x12\x15\n\rspeed_percent\x18- \x01(\x05\x12\r\n\x05speed\x18. \x01(\x05\x12\x15\n\rwhoosh_enable\x18: \x01(\x08\x12\x12\n\neco_enable\x18\x41 \x01(\x08\x12\x1b\n\x13\x61uto_comfort_enable\x18/ \x01(\x08\x12!\n\x19\x63omfort_ideal_temperature\x18\x30 \x01(\x05\x12\"\n\x1a\x63omfort_heat_assist_enable\x18< \x01(\x08\x12!\n\x19\x63omfort_heat_assist_speed\x18= \x01(\x05\x12*\n\"comfort_heat_assist_reverse_enable\x18> \x01(\x08\x12\x19\n\x11\x63omfort_min_speed\x18\x32 \x01(\x05\x12\x19\n\x11\x63omfort_max_speed\x18\x33 \x01(\x05\x12\x1b\n\x13motion_sense_enable\x18\x34 \x01(\x08\x12\x1c\n\x14motion_sense_timeout\x18\x35 \x01(\x05\x12\x1d\n\x15return_to_auto_enable\x18\x36 \x01(\x08\x12\x1e\n\x16return_to_auto_timeout\x18\x37 \x01(\x05\x12\x12\n\ntarget_rpm\x18? \x01(\x05\x12\x13\n\x0b\x63urrent_rpm\x18@ \x01(\x05\x12\'\n\nlight_mode\x18\x44 \x01(\x0e\x32\x13.aiobafi6.OffOnAuto\x12 \n\x18light_brightness_percent\x18\x45 \x01(\x05\x12\x1e\n\x16light_brightness_level\x18\x46 \x01(\x05\x12\x1f\n\x17light_color_temperature\x18G \x01(\x05\x12 \n\x18light_dim_to_warm_enable\x18M \x01(\x08\x12!\n\x19light_auto_motion_timeout\x18I \x01(\x05\x12#\n\x1blight_return_to_auto_enable\x18J \x01(\x08\x12$\n\x1clight_return_to_auto_timeout\x18K \x01(\x05\x12\'\n\x1flight_warmest_color_temperature\x18N \x01(\x05\x12\'\n\x1flight_coolest_color_temperature\x18O \x01(\x05\x12\x13\n\x0btemperature\x18V \x01(\x05\x12\x10\n\x08humidity\x18W \x01(\x05\x12\x12\n\nip_address\x18x \x01(\t\x12&\n\x04wifi\x18| \x01(\x0b\x32\x18.aiobafi6.WifiProperties\x12\x1e\n\x15led_indicators_enable\x18\x86\x01 \x01(\x08\x12\x18\n\x0f\x66\x61n_beep_enable\x18\x87\x01 \x01(\x08\x12 \n\x17legacy_ir_remote_enable\x18\x88\x01 \x01(\x08\x12\x36\n\x0fremote_firmware\x18\x98\x01 \x01(\x0b\x32\x1c.aiobafi6.FirmwareProperties\x12\x1f\n\x05stats\x18\x9c\x01 \x01(\x0b\x32\x0f.aiobafi6.Stats\"_\n\x12\x46irmwareProperties\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x1a\n\x12\x62ootloader_version\x18\x03 \x01(\t\x12\x13\n\x0bmac_address\x18\x04 \x01(\t\"!\n\x0c\x43\x61pabilities\x12\x11\n\thas_light\x18\x04 \x01(\x08\"\x1e\n\x0eWifiProperties\x12\x0c\n\x04ssid\x18\x01 \x01(\t\"\n\n\x08Schedule\"y\n\x05Stats\x12\x16\n\x0euptime_minutes\x18\x01 \x01(\x05\x12\x10\n\x08unknown2\x18\x02 \x01(\x05\x12\x10\n\x08unknown3\x18\x03 \x01(\x05\x12\x10\n\x08unknown4\x18\x04 \x01(\x05\x12\x10\n\x08unknown5\x18\x05 \x01(\x05\x12\x10\n\x08unknown6\x18\x06 \x01(\x05*t\n\x0cProperyQuery\x12\x07\n\x03\x41LL\x10\x00\x12\x07\n\x03\x46\x41N\x10\x01\x12\t\n\x05LIGHT\x10\x02\x12\x1e\n\x1a\x46IRMWARE_MORE_DATETIME_API\x10\x03\x12\x0b\n\x07NETWORK\x10\x04\x12\r\n\tSCHEDULES\x10\x05\x12\x0b\n\x07SENSORS\x10\x06*&\n\tOffOnAuto\x12\x07\n\x03OFF\x10\x00\x12\x06\n\x02ON\x10\x01\x12\x08\n\x04\x41UTO\x10\x02') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14proto/aiobafi6.proto\x12\x08\x61iobafi6\"&\n\x04Root\x12\x1e\n\x05root2\x18\x02 \x01(\x0b\x32\x0f.aiobafi6.Root2\"v\n\x05Root2\x12 \n\x06\x63ommit\x18\x02 \x01(\x0b\x32\x10.aiobafi6.Commit\x12\x1e\n\x05query\x18\x03 \x01(\x0b\x32\x0f.aiobafi6.Query\x12+\n\x0cquery_result\x18\x04 \x01(\x0b\x32\x15.aiobafi6.QueryResult\"2\n\x06\x43ommit\x12(\n\nproperties\x18\x03 \x01(\x0b\x32\x14.aiobafi6.Properties\"7\n\x05Query\x12.\n\x0eproperty_query\x18\x01 \x01(\x0e\x32\x16.aiobafi6.ProperyQuery\"^\n\x0bQueryResult\x12(\n\nproperties\x18\x02 \x03(\x0b\x32\x14.aiobafi6.Properties\x12%\n\tschedules\x18\x03 \x03(\x0b\x32\x12.aiobafi6.Schedule\"\xd5\x0b\n\nProperties\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x16\n\x0elocal_datetime\x18\x04 \x01(\t\x12\x14\n\x0cutc_datetime\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x07 \x01(\t\x12\x13\n\x0bmac_address\x18\x08 \x01(\t\x12\r\n\x05uuid9\x18\t \x01(\t\x12\x13\n\x0b\x64ns_sd_uuid\x18\n \x01(\t\x12\x14\n\x0c\x61pi_endpoint\x18\x0b \x01(\t\x12\x13\n\x0b\x61pi_version\x18\r \x01(\t\x12.\n\x08\x66irmware\x18\x10 \x01(\x0b\x32\x1c.aiobafi6.FirmwareProperties\x12,\n\x0c\x63\x61pabilities\x18\x11 \x01(\x0b\x32\x16.aiobafi6.Capabilities\x12%\n\x08\x66\x61n_mode\x18+ \x01(\x0e\x32\x13.aiobafi6.OffOnAuto\x12\x16\n\x0ereverse_enable\x18, \x01(\x08\x12\x15\n\rspeed_percent\x18- \x01(\x05\x12\r\n\x05speed\x18. \x01(\x05\x12\x15\n\rwhoosh_enable\x18: \x01(\x08\x12\x12\n\neco_enable\x18\x41 \x01(\x08\x12\x1b\n\x13\x61uto_comfort_enable\x18/ \x01(\x08\x12!\n\x19\x63omfort_ideal_temperature\x18\x30 \x01(\x05\x12\"\n\x1a\x63omfort_heat_assist_enable\x18< \x01(\x08\x12!\n\x19\x63omfort_heat_assist_speed\x18= \x01(\x05\x12*\n\"comfort_heat_assist_reverse_enable\x18> \x01(\x08\x12\x19\n\x11\x63omfort_min_speed\x18\x32 \x01(\x05\x12\x19\n\x11\x63omfort_max_speed\x18\x33 \x01(\x05\x12\x1b\n\x13motion_sense_enable\x18\x34 \x01(\x08\x12\x1c\n\x14motion_sense_timeout\x18\x35 \x01(\x05\x12\x1d\n\x15return_to_auto_enable\x18\x36 \x01(\x08\x12\x1e\n\x16return_to_auto_timeout\x18\x37 \x01(\x05\x12\x12\n\ntarget_rpm\x18? \x01(\x05\x12\x13\n\x0b\x63urrent_rpm\x18@ \x01(\x05\x12\'\n\nlight_mode\x18\x44 \x01(\x0e\x32\x13.aiobafi6.OffOnAuto\x12 \n\x18light_brightness_percent\x18\x45 \x01(\x05\x12\x1e\n\x16light_brightness_level\x18\x46 \x01(\x05\x12\x1f\n\x17light_color_temperature\x18G \x01(\x05\x12 \n\x18light_dim_to_warm_enable\x18M \x01(\x08\x12!\n\x19light_auto_motion_timeout\x18I \x01(\x05\x12#\n\x1blight_return_to_auto_enable\x18J \x01(\x08\x12$\n\x1clight_return_to_auto_timeout\x18K \x01(\x05\x12\'\n\x1flight_warmest_color_temperature\x18N \x01(\x05\x12\'\n\x1flight_coolest_color_temperature\x18O \x01(\x05\x12\x13\n\x0btemperature\x18V \x01(\x05\x12\x10\n\x08humidity\x18W \x01(\x05\x12\x12\n\nip_address\x18x \x01(\t\x12&\n\x04wifi\x18| \x01(\x0b\x32\x18.aiobafi6.WifiProperties\x12\x1e\n\x15led_indicators_enable\x18\x86\x01 \x01(\x08\x12\x18\n\x0f\x66\x61n_beep_enable\x18\x87\x01 \x01(\x08\x12 \n\x17legacy_ir_remote_enable\x18\x88\x01 \x01(\x08\x12\x36\n\x0fremote_firmware\x18\x98\x01 \x01(\x0b\x32\x1c.aiobafi6.FirmwareProperties\x12\x1f\n\x05stats\x18\x9c\x01 \x01(\x0b\x32\x0f.aiobafi6.Stats\"_\n\x12\x46irmwareProperties\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x1a\n\x12\x62ootloader_version\x18\x03 \x01(\t\x12\x13\n\x0bmac_address\x18\x04 \x01(\t\"M\n\x0c\x43\x61pabilities\x12\x14\n\x0chas_comfort1\x18\x01 \x01(\x08\x12\x14\n\x0chas_comfort3\x18\x03 \x01(\x08\x12\x11\n\thas_light\x18\x04 \x01(\x08\"\x1e\n\x0eWifiProperties\x12\x0c\n\x04ssid\x18\x01 \x01(\t\"\n\n\x08Schedule\"y\n\x05Stats\x12\x16\n\x0euptime_minutes\x18\x01 \x01(\x05\x12\x10\n\x08unknown2\x18\x02 \x01(\x05\x12\x10\n\x08unknown3\x18\x03 \x01(\x05\x12\x10\n\x08unknown4\x18\x04 \x01(\x05\x12\x10\n\x08unknown5\x18\x05 \x01(\x05\x12\x10\n\x08unknown6\x18\x06 \x01(\x05*t\n\x0cProperyQuery\x12\x07\n\x03\x41LL\x10\x00\x12\x07\n\x03\x46\x41N\x10\x01\x12\t\n\x05LIGHT\x10\x02\x12\x1e\n\x1a\x46IRMWARE_MORE_DATETIME_API\x10\x03\x12\x0b\n\x07NETWORK\x10\x04\x12\r\n\tSCHEDULES\x10\x05\x12\x0b\n\x07SENSORS\x10\x06*&\n\tOffOnAuto\x12\x07\n\x03OFF\x10\x00\x12\x06\n\x02ON\x10\x01\x12\x08\n\x04\x41UTO\x10\x02') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'proto.aiobafi6_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _PROPERYQUERY._serialized_start=2194 - _PROPERYQUERY._serialized_end=2310 - _OFFONAUTO._serialized_start=2312 - _OFFONAUTO._serialized_end=2350 + _PROPERYQUERY._serialized_start=2238 + _PROPERYQUERY._serialized_end=2354 + _OFFONAUTO._serialized_start=2356 + _OFFONAUTO._serialized_end=2394 _ROOT._serialized_start=34 _ROOT._serialized_end=72 _ROOT2._serialized_start=74 @@ -39,11 +39,11 @@ _FIRMWAREPROPERTIES._serialized_start=1895 _FIRMWAREPROPERTIES._serialized_end=1990 _CAPABILITIES._serialized_start=1992 - _CAPABILITIES._serialized_end=2025 - _WIFIPROPERTIES._serialized_start=2027 - _WIFIPROPERTIES._serialized_end=2057 - _SCHEDULE._serialized_start=2059 - _SCHEDULE._serialized_end=2069 - _STATS._serialized_start=2071 - _STATS._serialized_end=2192 + _CAPABILITIES._serialized_end=2069 + _WIFIPROPERTIES._serialized_start=2071 + _WIFIPROPERTIES._serialized_end=2101 + _SCHEDULE._serialized_start=2103 + _SCHEDULE._serialized_end=2113 + _STATS._serialized_start=2115 + _STATS._serialized_end=2236 # @@protoc_insertion_point(module_scope) diff --git a/aiobafi6/proto/aiobafi6_pb2.pyi b/aiobafi6/proto/aiobafi6_pb2.pyi index ff74c9f..b4177d4 100644 --- a/aiobafi6/proto/aiobafi6_pb2.pyi +++ b/aiobafi6/proto/aiobafi6_pb2.pyi @@ -17,10 +17,14 @@ SCHEDULES: ProperyQuery SENSORS: ProperyQuery class Capabilities(_message.Message): - __slots__ = ["has_light"] + __slots__ = ["has_comfort1", "has_comfort3", "has_light"] + HAS_COMFORT1_FIELD_NUMBER: ClassVar[int] + HAS_COMFORT3_FIELD_NUMBER: ClassVar[int] HAS_LIGHT_FIELD_NUMBER: ClassVar[int] + has_comfort1: bool + has_comfort3: bool has_light: bool - def __init__(self, has_light: bool = ...) -> None: ... + def __init__(self, has_comfort1: bool = ..., has_comfort3: bool = ..., has_light: bool = ...) -> None: ... class Commit(_message.Message): __slots__ = ["properties"] diff --git a/aiobafi6/wireutils.py b/aiobafi6/wireutils.py index e5cf662..d1e233a 100644 --- a/aiobafi6/wireutils.py +++ b/aiobafi6/wireutils.py @@ -1,32 +1,23 @@ -"""Utilities for BAF i6 wire serialization and deserialization.""" -from __future__ import annotations +"""Utilities for BAF message encoding and decoding. -from .proto import aiobafi6_pb2 +BAF uses SLIP (Serial Line IP, https://datatracker.ietf.org/doc/html/rfc1055.html) to +frame protocol buffer messages on a TCP/IP stream connection. +""" +from __future__ import annotations +from google.protobuf.message import Message -def serialize(root: aiobafi6_pb2.Root) -> bytes: - """Serializes a message for transmission. - This will serialize the root proto message, apply emulation prevention sequences - and add the `0xc0` framing bytes. - """ - buf = bytearray([0xc0]) - buf.extend(add_emulation_prevention(root.SerializeToString())) - buf.append(0xc0) +def serialize(message: Message) -> bytes: + """Serialize `message` to bytes and put it in a SLIP frame.""" + buf = bytearray([0xC0]) + buf.extend(add_emulation_prevention(message.SerializeToString())) + buf.append(0xC0) return buf def add_emulation_prevention(buf: bytes) -> bytes: - """Adds emulation prevention sequences. - - The BAF i6 protocol frames its messages on a stream connection using a pair of 0xc0 - bytes. In case a message payload contains 0xc0 bytes, all such bytes are replaced - with a so-called emulation prevention sequence (`0xdb 0xdc`). In case a message - payload contains this emulation prevention sequence itself, all `0xdb` bytes are - replaced with a separate emulation prevention sequence (`0xdb 0xdd`). - - This function adds all such emulation prevention sequences. - """ + """Add emulation prevention sequences (SLIP ESC).""" o = bytearray() for b in buf: if b == 0xC0: @@ -39,16 +30,7 @@ def add_emulation_prevention(buf: bytes) -> bytes: def remove_emulation_prevention(buf: bytes) -> bytes: - """Removes emulation prevention sequences. - - The BAF i6 protocol frames its messages on a stream connection using a pair of 0xc0 - bytes. In case a message payload contains 0xc0 bytes, all such bytes are replaced - with a so-called emulation prevention sequence (`0xdb 0xdc`). In case a message - payload contains this emulation prevention sequence itself, all `0xdb` bytes are - replaced with a separate emulation prevention sequence (`0xdb 0xdd`). - - This function removes all such emulation prevention sequences. - """ + """Remove emulation prevention sequences (SLIP ESC).""" o = bytearray() eps = False for b in buf: diff --git a/proto/aiobafi6.proto b/proto/aiobafi6.proto index 6b43dfb..0a031a6 100644 --- a/proto/aiobafi6.proto +++ b/proto/aiobafi6.proto @@ -121,7 +121,11 @@ message FirmwareProperties { optional string mac_address = 4; } -message Capabilities { optional bool has_light = 4; } +message Capabilities { + optional bool has_comfort1 = 1; + optional bool has_comfort3 = 3; + optional bool has_light = 4; +} message WifiProperties { optional string ssid = 1; } diff --git a/pyproject.toml b/pyproject.toml index c7e7cbc..a5ab59a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aiobafi6" -version = "0.4.0" +version = "0.5.0" description = "Big Ass Fans i6/Haiku protocol asynchronous Python library" authors = ["Jean-Francois Roy "] license = "Apache-2.0" @@ -25,6 +25,7 @@ isort = "^5.10.1" poethepoet = "^0.13.1" pytest = "^7.1.2" pytest-asyncio = "^0.18" +flake8 = "^4.0.1" [tool.poetry.scripts] aiobafi6 = 'aiobafi6.cmd.main:main'