From 4fd2b1681cd416277698dbc5fe3e8750a888e18b Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:00:42 -0800 Subject: [PATCH 01/26] Add GH Actions --- .github/workflows/flake8.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/flake8.yaml diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml new file mode 100644 index 00000000..1138157b --- /dev/null +++ b/.github/workflows/flake8.yaml @@ -0,0 +1,33 @@ +name: Python flake8 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.4, 3.5, 3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 wemake-python-styleguide + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --statistics From ebaac1f5731c768d1219a73b691395ff8b79d3d2 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:02:34 -0800 Subject: [PATCH 02/26] Fix branches --- .github/workflows/flake8.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 1138157b..3fad765e 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -2,9 +2,9 @@ name: Python flake8 on: push: - branches: [ main ] + branches: [ main, master, dev, development ] pull_request: - branches: [ main ] + branches: [ main, master, dev, development ] jobs: build: From 97077d31b27768a9eaa851d8e7ae80578aed38b2 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:07:32 -0800 Subject: [PATCH 03/26] Conditionals --- .github/workflows/flake8.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 3fad765e..3cd86189 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -7,13 +7,11 @@ on: branches: [ main, master, dev, development ] jobs: - build: - + test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.4, 3.5, 3.6, 3.7, 3.8, 3.9] - steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -23,8 +21,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 wemake-python-styleguide + pip install flake8 if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Install wemake-python-styleguide + run: pip install wemake-python-styleguide + if: endsWith( matrix.python-version, '6') - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 25856e31297ab4425b3960802881e4eec9ff3445 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:08:58 -0800 Subject: [PATCH 04/26] Update flake8.yaml --- .github/workflows/flake8.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 3cd86189..67c588b8 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -25,7 +25,7 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Install wemake-python-styleguide run: pip install wemake-python-styleguide - if: endsWith( matrix.python-version, '6') + if: ! (endsWith( matrix.python-version, '4') || endsWith( matrix.python-version, '5')) - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From d09f5016f2dbb876c846183f49c8ac38e2e89f7d Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:14:24 -0800 Subject: [PATCH 05/26] Go back --- .github/workflows/flake8.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 67c588b8..d52689cf 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.4, 3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -21,11 +21,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 + pip install flake8 wemake-python-styleguide if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Install wemake-python-styleguide - run: pip install wemake-python-styleguide - if: ! (endsWith( matrix.python-version, '4') || endsWith( matrix.python-version, '5')) - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 9482804c8fb322b2d5816db9e29c08df32de53fb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:18:51 -0800 Subject: [PATCH 06/26] Update flake8.yaml --- .github/workflows/flake8.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index d52689cf..ba0f6e70 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -27,5 +27,5 @@ jobs: run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --statistics + # exit-zero treats all errors as warnings. ignore magic numbers and use double quotes and ignore numbers with zeroes before them. + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --ignore=WPS432,WPS339 --inline-quotes double --statistics From 153b96fd45c1f7fa75d64eecc7f976a9aa1bc384 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:22:08 -0800 Subject: [PATCH 07/26] Update flake8.yaml --- .github/workflows/flake8.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index ba0f6e70..2fd67945 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -28,4 +28,5 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. ignore magic numbers and use double quotes and ignore numbers with zeroes before them. - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --ignore=WPS432,WPS339 --inline-quotes double --statistics + # and ignore lowercase hex numbers and ignore isort incorrect imports + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --ignore=WPS432,WPS339,WPS341,I --inline-quotes double --statistics From 922eb45e17153884a6be21f2e5f90e8056758f14 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:25:33 -0800 Subject: [PATCH 08/26] Run black --- broadlink/__init__.py | 147 +++++++++++++++++++--------------------- broadlink/alarm.py | 28 ++++---- broadlink/climate.py | 124 ++++++++++++++++++++++------------ broadlink/cover.py | 8 +-- broadlink/device.py | 154 +++++++++++++++++++++--------------------- broadlink/light.py | 54 ++++++++------- broadlink/remote.py | 36 +++++----- broadlink/sensor.py | 20 +++--- broadlink/switch.py | 114 +++++++++++++++++-------------- setup.py | 24 +++---- 10 files changed, 379 insertions(+), 330 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 604ed88a..74ae1fa6 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -20,88 +20,88 @@ 0x2716: (sp2, "NEO PRO", "Ankuoo"), 0x2717: (sp2, "NEO", "Ankuoo"), 0x2719: (sp2, "SP2-compatible", "Honeywell"), - 0x271a: (sp2, "SP2-compatible", "Honeywell"), + 0x271A: (sp2, "SP2-compatible", "Honeywell"), 0x2720: (sp2, "SP mini", "Broadlink"), 0x2728: (sp2, "SP2-compatible", "URANT"), 0x2733: (sp2, "SP3", "Broadlink"), 0x2736: (sp2, "SP mini+", "Broadlink"), - 0x273e: (sp2, "SP mini", "Broadlink"), + 0x273E: (sp2, "SP mini", "Broadlink"), 0x7530: (sp2, "SP2", "Broadlink (OEM)"), 0x7539: (sp2, "SP2-IL", "Broadlink (OEM)"), - 0x753e: (sp2, "SP mini 3", "Broadlink"), + 0x753E: (sp2, "SP mini 3", "Broadlink"), 0x7540: (sp2, "MP2", "Broadlink"), - 0X7544: (sp2, "SP2-CL", "Broadlink"), + 0x7544: (sp2, "SP2-CL", "Broadlink"), 0x7546: (sp2, "SP2-UK/BR/IN", "Broadlink (OEM)"), 0x7547: (sp2, "SC1", "Broadlink"), 0x7918: (sp2, "SP2", "Broadlink (OEM)"), 0x7919: (sp2, "SP2-compatible", "Honeywell"), - 0x791a: (sp2, "SP2-compatible", "Honeywell"), - 0x7d00: (sp2, "SP3-EU", "Broadlink (OEM)"), - 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), + 0x791A: (sp2, "SP2-compatible", "Honeywell"), + 0x7D00: (sp2, "SP3-EU", "Broadlink (OEM)"), + 0x7D0D: (sp2, "SP mini 3", "Broadlink (OEM)"), 0x9479: (sp2, "SP3S-US", "Broadlink"), - 0x947a: (sp2, "SP3S-EU", "Broadlink"), - 0x756c: (sp4, "SP4M", "Broadlink"), + 0x947A: (sp2, "SP3S-EU", "Broadlink"), + 0x756C: (sp4, "SP4M", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), 0x7583: (sp4, "SP mini 3", "Broadlink"), - 0x7d11: (sp4, "SP mini 3", "Broadlink"), - 0x648b: (sp4b, "SP4M-US", "Broadlink"), + 0x7D11: (sp4, "SP mini 3", "Broadlink"), + 0x648B: (sp4b, "SP4M-US", "Broadlink"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), - 0x272a: (rm, "RM pro", "Broadlink"), + 0x272A: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), - 0x273d: (rm, "RM pro", "Broadlink"), - 0x277c: (rm, "RM home", "Broadlink"), + 0x273D: (rm, "RM pro", "Broadlink"), + 0x277C: (rm, "RM home", "Broadlink"), 0x2783: (rm, "RM home", "Broadlink"), 0x2787: (rm, "RM pro", "Broadlink"), - 0x278b: (rm, "RM plus", "Broadlink"), - 0x278f: (rm, "RM mini", "Broadlink"), + 0x278B: (rm, "RM plus", "Broadlink"), + 0x278F: (rm, "RM mini", "Broadlink"), 0x2797: (rm, "RM pro+", "Broadlink"), - 0x279d: (rm, "RM pro+", "Broadlink"), - 0x27a1: (rm, "RM plus", "Broadlink"), - 0x27a6: (rm, "RM plus", "Broadlink"), - 0x27a9: (rm, "RM pro+", "Broadlink"), - 0x27c2: (rm, "RM mini 3", "Broadlink"), - 0x27c3: (rm, "RM pro+", "Broadlink"), - 0x27c7: (rm, "RM mini 3", "Broadlink"), - 0x27cc: (rm, "RM mini 3", "Broadlink"), - 0x27cd: (rm, "RM mini 3", "Broadlink"), - 0x27d0: (rm, "RM mini 3", "Broadlink"), - 0x27d1: (rm, "RM mini 3", "Broadlink"), - 0x27de: (rm, "RM mini 3", "Broadlink"), - 0x51da: (rm4, "RM4 mini", "Broadlink"), - 0x5f36: (rm4, "RM mini 3", "Broadlink"), + 0x279D: (rm, "RM pro+", "Broadlink"), + 0x27A1: (rm, "RM plus", "Broadlink"), + 0x27A6: (rm, "RM plus", "Broadlink"), + 0x27A9: (rm, "RM pro+", "Broadlink"), + 0x27C2: (rm, "RM mini 3", "Broadlink"), + 0x27C3: (rm, "RM pro+", "Broadlink"), + 0x27C7: (rm, "RM mini 3", "Broadlink"), + 0x27CC: (rm, "RM mini 3", "Broadlink"), + 0x27CD: (rm, "RM mini 3", "Broadlink"), + 0x27D0: (rm, "RM mini 3", "Broadlink"), + 0x27D1: (rm, "RM mini 3", "Broadlink"), + 0x27DE: (rm, "RM mini 3", "Broadlink"), + 0x51DA: (rm4, "RM4 mini", "Broadlink"), + 0x5F36: (rm4, "RM mini 3", "Broadlink"), 0x6026: (rm4, "RM4 pro", "Broadlink"), 0x6070: (rm4, "RM4C mini", "Broadlink"), - 0x610e: (rm4, "RM4 mini", "Broadlink"), - 0x610f: (rm4, "RM4C mini", "Broadlink"), - 0x61a2: (rm4, "RM4 pro", "Broadlink"), - 0x62bc: (rm4, "RM4 mini", "Broadlink"), - 0x62be: (rm4, "RM4C mini", "Broadlink"), - 0x648d: (rm4, "RM4 mini", "Broadlink"), - 0x649b: (rm4, "RM4 pro", "Broadlink"), - 0x653a: (rm4, "RM4 mini", "Broadlink"), + 0x610E: (rm4, "RM4 mini", "Broadlink"), + 0x610F: (rm4, "RM4C mini", "Broadlink"), + 0x61A2: (rm4, "RM4 pro", "Broadlink"), + 0x62BC: (rm4, "RM4 mini", "Broadlink"), + 0x62BE: (rm4, "RM4C mini", "Broadlink"), + 0x648D: (rm4, "RM4 mini", "Broadlink"), + 0x649B: (rm4, "RM4 pro", "Broadlink"), + 0x653A: (rm4, "RM4 mini", "Broadlink"), 0x2714: (a1, "e-Sensor", "Broadlink"), - 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), - 0x4ef7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), - 0x4f1b: (mp1, "MP1-1K3S2U", "Broadlink (OEM)"), - 0x4f65: (mp1, "MP1-1K3S2U", "Broadlink"), + 0x4EB5: (mp1, "MP1-1K4S", "Broadlink"), + 0x4EF7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), + 0x4F1B: (mp1, "MP1-1K3S2U", "Broadlink (OEM)"), + 0x4F65: (mp1, "MP1-1K3S2U", "Broadlink"), 0x5043: (lb1, "SB800TD", "Broadlink (OEM)"), - 0x504e: (lb1, "LB1", "Broadlink"), - 0x60c7: (lb1, "LB1", "Broadlink"), - 0x60c8: (lb1, "LB1", "Broadlink"), + 0x504E: (lb1, "LB1", "Broadlink"), + 0x60C7: (lb1, "LB1", "Broadlink"), + 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), - 0x4ead: (hysen, "HY02B05H", "Hysen"), - 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), - 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), + 0x4EAD: (hysen, "HY02B05H", "Hysen"), + 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), + 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), } def gendevice( - dev_type: int, - host: Tuple[str, int], - mac: Union[bytes, str], - name: str = None, - is_locked: bool = None, + dev_type: int, + host: Tuple[str, int], + mac: Union[bytes, str], + name: str = None, + is_locked: bool = None, ) -> device: """Generate a device.""" try: @@ -122,10 +122,10 @@ def gendevice( def hello( - host: str, - port: int = 80, - timeout: int = 10, - local_ip_address: str = None, + host: str, + port: int = 80, + timeout: int = 10, + local_ip_address: str = None, ) -> device: """Direct device discovery. @@ -138,31 +138,27 @@ def hello( def discover( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> List[device]: """Discover devices connected to the local network.""" - responses = scan( - timeout, local_ip_address, discover_ip_address, discover_ip_port - ) + responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) return [gendevice(*resp) for resp in responses] def xdiscover( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> Generator[device, None, None]: """Discover devices connected to the local network. This function returns a generator that yields devices instantly. """ - responses = scan( - timeout, local_ip_address, discover_ip_address, discover_ip_port - ) + responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) for resp in responses: yield gendevice(*resp) @@ -191,13 +187,12 @@ def setup(ssid: str, password: str, security_mode: int) -> None: payload[0x85] = pass_length # Character length of password payload[0x86] = security_mode # Type of encryption - checksum = sum(payload, 0xbeaf) & 0xffff - payload[0x20] = checksum & 0xff # Checksum 1 position + checksum = sum(payload, 0xBEAF) & 0xFFFF + payload[0x20] = checksum & 0xFF # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position - sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet # UDP sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.sendto(payload, ('255.255.255.255', 80)) + sock.sendto(payload, ("255.255.255.255", 80)) sock.close() diff --git a/broadlink/alarm.py b/broadlink/alarm.py index faded9d4..e73b8fad 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -7,21 +7,21 @@ class S1C(device): """Controls a Broadlink S1C.""" _SENSORS_TYPES = { - 0x31: 'Door Sensor', # 49 as hex - 0x91: 'Key Fob', # 145 as hex, as serial on fob corpse - 0x21: 'Motion Sensor' # 33 as hex + 0x31: "Door Sensor", # 49 as hex + 0x91: "Key Fob", # 145 as hex, as serial on fob corpse + 0x21: "Motion Sensor", # 33 as hex } def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) - self.type = 'S1C' + self.type = "S1C" def get_sensors_status(self) -> dict: """Return the state of the sensors.""" packet = bytearray(16) packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if not payload: @@ -29,20 +29,20 @@ def get_sensors_status(self) -> dict: count = payload[0x4] sensor_data = payload[0x6:] sensors = [ - bytearray(sensor_data[i * 83:(i + 1) * 83]) + bytearray(sensor_data[i * 83 : (i + 1) * 83]) for i in range(len(sensor_data) // 83) ] return { - 'count': count, - 'sensors': [ + "count": count, + "sensors": [ { - 'status': sensor[0], - 'name': sensor[4:26].decode().strip('\x00'), - 'type': self._SENSORS_TYPES.get(sensor[3], 'Unknown'), - 'order': sensor[1], - 'serial': sensor[26:30].hex(), + "status": sensor[0], + "name": sensor[4:26].decode().strip("\x00"), + "type": self._SENSORS_TYPES.get(sensor[3], "Unknown"), + "order": sensor[1], + "serial": sensor[26:30].hex(), } for sensor in sensors if any(sensor[26:30]) - ] + ], } diff --git a/broadlink/climate.py b/broadlink/climate.py index f0c337e5..036cc9a8 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -33,19 +33,22 @@ def send_request(self, input_payload: bytes) -> bytes: request_payload.append((crc >> 8) & 0xFF) # send to device - response = self.send_packet(0x6a, request_payload) + response = self.send_packet(0x6A, request_payload) check_error(response[0x22:0x24]) response_payload = self.decrypt(response[0x38:]) # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc) response_payload_len = response_payload[0] if response_payload_len + 2 > len(response_payload): - raise ValueError('hysen_response_error', 'first byte of response is not length') + raise ValueError( + "hysen_response_error", "first byte of response is not length" + ) crc = calculate_crc16(response_payload[2:response_payload_len]) if (response_payload[response_payload_len] == crc & 0xFF) and ( - response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF): + response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF + ): return response_payload[2:response_payload_len] - raise ValueError('hysen_response_error', 'CRC check on response failed') + raise ValueError("hysen_response_error", "CRC check on response failed") def get_temp(self) -> int: """Return the room temperature in degrees celsius.""" @@ -64,43 +67,53 @@ def get_full_status(self) -> dict: """ payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16])) data = {} - data['remote_lock'] = payload[3] & 1 - data['power'] = payload[4] & 1 - data['active'] = (payload[4] >> 4) & 1 - data['temp_manual'] = (payload[4] >> 6) & 1 - data['room_temp'] = (payload[5] & 255) / 2.0 - data['thermostat_temp'] = (payload[6] & 255) / 2.0 - data['auto_mode'] = payload[7] & 15 - data['loop_mode'] = (payload[7] >> 4) & 15 - data['sensor'] = payload[8] - data['osv'] = payload[9] - data['dif'] = payload[10] - data['svh'] = payload[11] - data['svl'] = payload[12] - data['room_temp_adj'] = ((payload[13] << 8) + payload[14]) / 2.0 - if data['room_temp_adj'] > 32767: - data['room_temp_adj'] = 32767 - data['room_temp_adj'] - data['fre'] = payload[15] - data['poweron'] = payload[16] - data['unknown'] = payload[17] - data['external_temp'] = (payload[18] & 255) / 2.0 - data['hour'] = payload[19] - data['min'] = payload[20] - data['sec'] = payload[21] - data['dayofweek'] = payload[22] + data["remote_lock"] = payload[3] & 1 + data["power"] = payload[4] & 1 + data["active"] = (payload[4] >> 4) & 1 + data["temp_manual"] = (payload[4] >> 6) & 1 + data["room_temp"] = (payload[5] & 255) / 2.0 + data["thermostat_temp"] = (payload[6] & 255) / 2.0 + data["auto_mode"] = payload[7] & 15 + data["loop_mode"] = (payload[7] >> 4) & 15 + data["sensor"] = payload[8] + data["osv"] = payload[9] + data["dif"] = payload[10] + data["svh"] = payload[11] + data["svl"] = payload[12] + data["room_temp_adj"] = ((payload[13] << 8) + payload[14]) / 2.0 + if data["room_temp_adj"] > 32767: + data["room_temp_adj"] = 32767 - data["room_temp_adj"] + data["fre"] = payload[15] + data["poweron"] = payload[16] + data["unknown"] = payload[17] + data["external_temp"] = (payload[18] & 255) / 2.0 + data["hour"] = payload[19] + data["min"] = payload[20] + data["sec"] = payload[21] + data["dayofweek"] = payload[22] weekday = [] for i in range(0, 6): weekday.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekday'] = weekday + { + "start_hour": payload[2 * i + 23], + "start_minute": payload[2 * i + 24], + "temp": payload[i + 39] / 2.0, + } + ) + + data["weekday"] = weekday weekend = [] for i in range(6, 8): weekend.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekend'] = weekend + { + "start_hour": payload[2 * i + 23], + "start_minute": payload[2 * i + 24], + "temp": payload[i + 39] / 2.0, + } + ) + + data["weekend"] = weekend return data # Change controller mode @@ -140,8 +153,27 @@ def set_advanced( poweron: int, ) -> None: """Set advanced options.""" - input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, - (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron]) + input_payload = bytearray( + [ + 0x01, + 0x10, + 0x00, + 0x02, + 0x00, + 0x05, + 0x0A, + loop_mode, + sensor, + osv, + dif, + svh, + svl, + (int(adj * 2) >> 8 & 0xFF), + (int(adj * 2) & 0xFF), + fre, + poweron, + ] + ) self.send_request(input_payload) # For backwards compatibility only. Prefer calling set_mode directly. @@ -169,7 +201,11 @@ def set_power(self, power: int = 1, remote_lock: int = 0) -> None: # n.b. day=1 is Monday, ..., day=7 is Sunday def set_time(self, hour: int, minute: int, second: int, day: int) -> None: """Set the time.""" - self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day])) + self.send_request( + bytearray( + [0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day] + ) + ) # Set timer schedule # Format is the same as you get from get_full_status. @@ -180,25 +216,25 @@ def set_time(self, hour: int, minute: int, second: int, day: int) -> None: def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: """Set timer schedule.""" # Begin with some magic values ... - input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18]) + input_payload = bytearray([0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18]) # Now simply append times/temps # weekday times for i in range(0, 6): - input_payload.append(weekday[i]['start_hour']) - input_payload.append(weekday[i]['start_minute']) + input_payload.append(weekday[i]["start_hour"]) + input_payload.append(weekday[i]["start_minute"]) # weekend times for i in range(0, 2): - input_payload.append(weekend[i]['start_hour']) - input_payload.append(weekend[i]['start_minute']) + input_payload.append(weekend[i]["start_hour"]) + input_payload.append(weekend[i]["start_minute"]) # weekday temperatures for i in range(0, 6): - input_payload.append(int(weekday[i]['temp'] * 2)) + input_payload.append(int(weekday[i]["temp"] * 2)) # weekend temperatures for i in range(0, 2): - input_payload.append(int(weekend[i]['temp'] * 2)) + input_payload.append(int(weekend[i]["temp"] * 2)) self.send_request(input_payload) diff --git a/broadlink/cover.py b/broadlink/cover.py index 236e747c..2691fe97 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -17,12 +17,12 @@ def _send(self, magic1: int, magic2: int) -> int: """Send a packet to the device.""" packet = bytearray(16) packet[0] = 0x09 - packet[2] = 0xbb + packet[2] = 0xBB packet[3] = magic1 packet[4] = magic2 - packet[9] = 0xfa + packet[9] = 0xFA packet[10] = 0x44 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[4] @@ -41,7 +41,7 @@ def stop(self) -> int: def get_percentage(self) -> int: """Return the position of the curtain.""" - return self._send(0x06, 0x5d) + return self._send(0x06, 0x5D) def set_percentage_and_wait(self, new_percentage: int) -> None: """Set the position of the curtain.""" diff --git a/broadlink/device.py b/broadlink/device.py index 07af618b..452abbee 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -15,10 +15,10 @@ def scan( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -36,38 +36,38 @@ def scan( timezone = int(time.timezone / -3600) if timezone < 0: - packet[0x08] = 0xff + timezone - 1 - packet[0x09] = 0xff - packet[0x0a] = 0xff - packet[0x0b] = 0xff + packet[0x08] = 0xFF + timezone - 1 + packet[0x09] = 0xFF + packet[0x0A] = 0xFF + packet[0x0B] = 0xFF else: packet[0x08] = timezone packet[0x09] = 0 - packet[0x0a] = 0 - packet[0x0b] = 0 + packet[0x0A] = 0 + packet[0x0B] = 0 year = datetime.now().year - packet[0x0c] = year & 0xff - packet[0x0d] = year >> 8 - packet[0x0e] = datetime.now().minute - packet[0x0f] = datetime.now().hour + packet[0x0C] = year & 0xFF + packet[0x0D] = year >> 8 + packet[0x0E] = datetime.now().minute + packet[0x0F] = datetime.now().hour subyear = str(year)[2:] packet[0x10] = int(subyear) packet[0x11] = datetime.now().isoweekday() packet[0x12] = datetime.now().day packet[0x13] = datetime.now().month - address = local_ip_address.split('.') + address = local_ip_address.split(".") packet[0x18] = int(address[3]) packet[0x19] = int(address[2]) - packet[0x1a] = int(address[1]) - packet[0x1b] = int(address[0]) - packet[0x1c] = port & 0xff - packet[0x1d] = port >> 8 + packet[0x1A] = int(address[1]) + packet[0x1B] = int(address[0]) + packet[0x1C] = port & 0xFF + packet[0x1D] = port >> 8 packet[0x26] = 6 - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 starttime = time.time() @@ -85,12 +85,12 @@ def scan( break devtype = response[0x34] | response[0x35] << 8 - mac = bytes(reversed(response[0x3a:0x40])) + mac = bytes(reversed(response[0x3A:0x40])) if (host, mac, devtype) in discovered: continue discovered.append((host, mac, devtype)) - name = response[0x40:].split(b'\x00')[0].decode('utf-8') + name = response[0x40:].split(b"\x00")[0].decode("utf-8") is_locked = bool(response[-1]) yield devtype, host, mac, name, is_locked finally: @@ -101,33 +101,33 @@ class device: """Controls a Broadlink device.""" def __init__( - self, - host: Tuple[str, int], - mac: Union[bytes, str], - devtype: int, - timeout: int = 10, - name: str = None, - model: str = None, - manufacturer: str = None, - is_locked: bool = None, + self, + host: Tuple[str, int], + mac: Union[bytes, str], + devtype: int, + timeout: int = 10, + name: str = None, + model: str = None, + manufacturer: str = None, + is_locked: bool = None, ) -> None: """Initialize the controller.""" self.host = host self.mac = bytes.fromhex(mac) if isinstance(mac, str) else mac - self.devtype = devtype if devtype is not None else 0x272a + self.devtype = devtype if devtype is not None else 0x272A self.timeout = timeout self.name = name self.model = model self.manufacturer = manufacturer self.is_locked = is_locked - self.count = random.randrange(0xffff) - self.iv = bytes.fromhex('562e17996d093d28ddb3ba695a2e6f58') + self.count = random.randrange(0xFFFF) + self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") self.id = bytes(4) self.type = "Unknown" self.lock = threading.Lock() self.aes = None - key = bytes.fromhex('097628343fe99e23765c1513accf8b02') + key = bytes.fromhex("097628343fe99e23765c1513accf8b02") self.update_aes(key) def __repr__(self): @@ -138,7 +138,7 @@ def __repr__(self): hex(self.devtype), self.host[0], self.host[1], - ':'.join(format(x, '02x') for x in self.mac), + ":".join(format(x, "02x") for x in self.mac), self.name, "Locked" if self.is_locked else "Unlocked", ) @@ -168,24 +168,24 @@ def auth(self) -> bool: payload[0x07] = 0x31 payload[0x08] = 0x31 payload[0x09] = 0x31 - payload[0x0a] = 0x31 - payload[0x0b] = 0x31 - payload[0x0c] = 0x31 - payload[0x0d] = 0x31 - payload[0x0e] = 0x31 - payload[0x0f] = 0x31 + payload[0x0A] = 0x31 + payload[0x0B] = 0x31 + payload[0x0C] = 0x31 + payload[0x0D] = 0x31 + payload[0x0E] = 0x31 + payload[0x0F] = 0x31 payload[0x10] = 0x31 payload[0x11] = 0x31 payload[0x12] = 0x31 - payload[0x1e] = 0x01 - payload[0x2d] = 0x01 - payload[0x30] = ord('T') - payload[0x31] = ord('e') - payload[0x32] = ord('s') - payload[0x33] = ord('t') - payload[0x34] = ord(' ') - payload[0x35] = ord(' ') - payload[0x36] = ord('1') + payload[0x1E] = 0x01 + payload[0x2D] = 0x01 + payload[0x30] = ord("T") + payload[0x31] = ord("e") + payload[0x32] = ord("s") + payload[0x33] = ord("t") + payload[0x34] = ord(" ") + payload[0x35] = ord(" ") + payload[0x36] = ord("1") response = self.send_packet(0x65, payload) check_error(response[0x22:0x24]) @@ -226,7 +226,7 @@ def hello(self, local_ip_address=None) -> bool: def get_fwversion(self) -> int: """Get firmware version.""" packet = bytearray([0x68]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 @@ -234,20 +234,20 @@ def get_fwversion(self) -> int: def set_name(self, name: str) -> None: """Set device name.""" packet = bytearray(4) - packet += name.encode('utf-8') + packet += name.encode("utf-8") packet += bytearray(0x50 - len(packet)) packet[0x43] = bool(self.is_locked) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) self.name = name def set_lock(self, state: bool) -> None: """Lock/unlock the device.""" packet = bytearray(4) - packet += self.name.encode('utf-8') + packet += self.name.encode("utf-8") packet += bytearray(0x50 - len(packet)) packet[0x43] = bool(state) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) self.is_locked = bool(state) @@ -257,27 +257,27 @@ def get_type(self) -> str: def send_packet(self, command: int, payload: bytes) -> bytes: """Send a packet to the device.""" - self.count = (self.count + 1) & 0xffff + self.count = (self.count + 1) & 0xFFFF packet = bytearray(0x38) - packet[0x00] = 0x5a - packet[0x01] = 0xa5 - packet[0x02] = 0xaa + packet[0x00] = 0x5A + packet[0x01] = 0xA5 + packet[0x02] = 0xAA packet[0x03] = 0x55 - packet[0x04] = 0x5a - packet[0x05] = 0xa5 - packet[0x06] = 0xaa + packet[0x04] = 0x5A + packet[0x05] = 0xA5 + packet[0x06] = 0xAA packet[0x07] = 0x55 - packet[0x24] = self.devtype & 0xff + packet[0x24] = self.devtype & 0xFF packet[0x25] = self.devtype >> 8 packet[0x26] = command - packet[0x28] = self.count & 0xff + packet[0x28] = self.count & 0xFF packet[0x29] = self.count >> 8 - packet[0x2a] = self.mac[5] - packet[0x2b] = self.mac[4] - packet[0x2c] = self.mac[3] - packet[0x2d] = self.mac[2] - packet[0x2e] = self.mac[1] - packet[0x2f] = self.mac[0] + packet[0x2A] = self.mac[5] + packet[0x2B] = self.mac[4] + packet[0x2C] = self.mac[3] + packet[0x2D] = self.mac[2] + packet[0x2E] = self.mac[1] + packet[0x2F] = self.mac[0] packet[0x30] = self.id[3] packet[0x31] = self.id[2] packet[0x32] = self.id[1] @@ -289,16 +289,16 @@ def send_packet(self, command: int, payload: bytes) -> bytes: payload = bytearray(payload) payload += bytearray(padding) - checksum = sum(payload, 0xbeaf) & 0xffff - packet[0x34] = checksum & 0xff + checksum = sum(payload, 0xBEAF) & 0xFFFF + packet[0x34] = checksum & 0xFF packet[0x35] = checksum >> 8 payload = self.encrypt(payload) for i in range(len(payload)): packet.append(payload[i]) - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 start_time = time.time() @@ -322,7 +322,7 @@ def send_packet(self, command: int, payload: bytes) -> bytes: raise exception(-4007) # Length error. checksum = resp[0x20] | (resp[0x21] << 8) - if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: + if sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF != checksum: raise exception(-4008) # Checksum error. return resp diff --git a/broadlink/light.py b/broadlink/light.py index cffc77d5..adadfab5 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -11,14 +11,14 @@ class lb1(device): state_dict = [] effect_map_dict = { - 'lovely color': 0, - 'flashlight': 1, - 'lightning': 2, - 'color fading': 3, - 'color breathing': 4, - 'multicolor breathing': 5, - 'color jumping': 6, - 'multicolor jumping': 7, + "lovely color": 0, + "flashlight": 1, + "lightning": 2, + "color fading": 3, + "color breathing": 4, + "multicolor breathing": 5, + "color jumping": 6, + "multicolor jumping": 7, } def __init__(self, *args, **kwargs) -> None: @@ -26,36 +26,38 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "SmartBulb" - def send_command(self, command: str, type: str = 'set') -> None: + def send_command(self, command: str, type: str = "set") -> None: """Send a command to the device.""" - packet = bytearray(16+(int(len(command)/16) + 1)*16) - packet[0x00] = 0x0c + len(command) & 0xff - packet[0x02] = 0xa5 - packet[0x03] = 0xa5 - packet[0x04] = 0x5a - packet[0x05] = 0x5a - packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set - packet[0x09] = 0x0b - packet[0x0a] = len(command) - packet[0x0e:] = map(ord, command) + packet = bytearray(16 + (int(len(command) / 16) + 1) * 16) + packet[0x00] = 0x0C + len(command) & 0xFF + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set + packet[0x09] = 0x0B + packet[0x0A] = len(command) + packet[0x0E:] = map(ord, command) - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x06] = checksum & 0xff # Checksum 1 position + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x06] = checksum & 0xFF # Checksum 1 position packet[0x07] = checksum >> 8 # Checksum 2 position - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x36:0x38]) payload = self.decrypt(response[0x38:]) - responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) + responseLength = int(payload[0x0A]) | (int(payload[0x0B]) << 8) if responseLength > 0: - self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) + self.state_dict = json.loads(payload[0x0E : 0x0E + responseLength]) def set_json(self, jsonstr: str) -> str: """Send a command to the device and return state.""" reconvert = json.loads(jsonstr) - if 'bulb_sceneidx' in reconvert.keys(): - reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255) + if "bulb_sceneidx" in reconvert.keys(): + reconvert["bulb_sceneidx"] = self.effect_map_dict.get( + reconvert["bulb_sceneidx"], 255 + ) self.send_command(json.dumps(reconvert)) return json.dumps(self.state_dict) diff --git a/broadlink/remote.py b/broadlink/remote.py index 4a427f69..b6528d68 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -17,45 +17,45 @@ def check_data(self) -> bytes: """Return the last captured code.""" packet = bytearray(self._request_header) packet.append(0x04) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return payload[len(self._request_header) + 4:] + return payload[len(self._request_header) + 4 :] def send_data(self, data: bytes) -> None: """Send a code to the device.""" packet = bytearray(self._code_sending_header) packet += bytearray([0x02, 0x00, 0x00, 0x00]) packet += data - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def enter_learning(self) -> None: """Enter infrared learning mode.""" packet = bytearray(self._request_header) packet.append(0x03) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def sweep_frequency(self) -> None: """Sweep frequency.""" packet = bytearray(self._request_header) packet.append(0x19) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def cancel_sweep_frequency(self) -> None: """Cancel sweep frequency.""" packet = bytearray(self._request_header) - packet.append(0x1e) - response = self.send_packet(0x6a, packet) + packet.append(0x1E) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def check_frequency(self) -> bool: """Return True if the frequency was identified successfully.""" packet = bytearray(self._request_header) - packet.append(0x1a) - response = self.send_packet(0x6a, packet) + packet.append(0x1A) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: @@ -65,8 +65,8 @@ def check_frequency(self) -> bool: def find_rf_packet(self) -> bool: """Enter radiofrequency learning mode.""" packet = bytearray(self._request_header) - packet.append(0x1b) - response = self.send_packet(0x6a, packet) + packet.append(0x1B) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: @@ -77,10 +77,10 @@ def _check_sensors(self, command: int) -> bytes: """Return the state of the sensors in raw format.""" packet = bytearray(self._request_header) packet.append(command) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return bytearray(payload[len(self._request_header) + 4:]) + return bytearray(payload[len(self._request_header) + 4 :]) def check_temperature(self) -> int: """Return the temperature.""" @@ -90,7 +90,7 @@ def check_temperature(self) -> int: def check_sensors(self) -> dict: """Return the state of the sensors.""" data = self._check_sensors(0x1) - return {'temperature': data[0x0] + data[0x1] / 10.0} + return {"temperature": data[0x0] + data[0x1] / 10.0} class rm4(rm): @@ -100,8 +100,8 @@ def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM4" - self._request_header = b'\x04\x00' - self._code_sending_header = b'\xda\x00' + self._request_header = b"\x04\x00" + self._code_sending_header = b"\xda\x00" def check_temperature(self) -> int: """Return the temperature.""" @@ -117,6 +117,6 @@ def check_sensors(self) -> dict: """Return the state of the sensors.""" data = self._check_sensors(0x24) return { - 'temperature': data[0x0] + data[0x1] / 100.0, - 'humidity': data[0x2] + data[0x3] / 100.0 + "temperature": data[0x0] + data[0x1] / 100.0, + "humidity": data[0x2] + data[0x3] / 100.0, } diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 63c23b4b..ef7f6f12 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -7,9 +7,9 @@ class a1(device): """Controls a Broadlink A1.""" _SENSORS_AND_LEVELS = ( - ('light', ('dark', 'dim', 'normal', 'bright')), - ('air_quality', ('excellent', 'good', 'normal', 'bad')), - ('noise', ('quiet', 'normal', 'noisy')), + ("light", ("dark", "dim", "normal", "bright")), + ("air_quality", ("excellent", "good", "normal", "bad")), + ("noise", ("quiet", "normal", "noisy")), ) def __init__(self, *args, **kwargs) -> None: @@ -24,20 +24,20 @@ def check_sensors(self) -> dict: try: data[sensor] = levels[data[sensor]] except IndexError: - data[sensor] = 'unknown' + data[sensor] = "unknown" return data def check_sensors_raw(self) -> dict: """Return the state of the sensors in raw format.""" packet = bytearray([0x1]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) data = bytearray(payload[0x4:]) return { - 'temperature': data[0x0] + data[0x1] / 10.0, - 'humidity': data[0x2] + data[0x3] / 10.0, - 'light': data[0x4], - 'air_quality': data[0x6], - 'noise': data[0x8], + "temperature": data[0x0] + data[0x1] / 10.0, + "humidity": data[0x2] + data[0x3] / 10.0, + "light": data[0x4], + "air_quality": data[0x6], + "noise": data[0x8], } diff --git a/broadlink/switch.py b/broadlink/switch.py index efea03d7..96225451 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -17,19 +17,19 @@ def __init__(self, *args, **kwargs) -> None: def set_power_mask(self, sid_mask: int, state: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) - packet[0x00] = 0x0d - packet[0x02] = 0xa5 - packet[0x03] = 0xa5 - packet[0x04] = 0x5a - packet[0x05] = 0x5a - packet[0x06] = 0xb2 + ((sid_mask << 1) if state else sid_mask) - packet[0x07] = 0xc0 + packet[0x00] = 0x0D + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xB2 + ((sid_mask << 1) if state else sid_mask) + packet[0x07] = 0xC0 packet[0x08] = 0x02 - packet[0x0a] = 0x03 - packet[0x0d] = sid_mask - packet[0x0e] = sid_mask if state else 0 + packet[0x0A] = 0x03 + packet[0x0D] = sid_mask + packet[0x0E] = sid_mask if state else 0 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def set_power(self, sid: int, state: bool) -> None: @@ -40,30 +40,30 @@ def set_power(self, sid: int, state: bool) -> None: def check_power_raw(self) -> bool: """Return the power state of the device in raw format.""" packet = bytearray(16) - packet[0x00] = 0x0a - packet[0x02] = 0xa5 - packet[0x03] = 0xa5 - packet[0x04] = 0x5a - packet[0x05] = 0x5a - packet[0x06] = 0xae - packet[0x07] = 0xc0 + packet[0x00] = 0x0A + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xAE + packet[0x07] = 0xC0 packet[0x08] = 0x01 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return payload[0x0e] + return payload[0x0E] def check_power(self) -> dict: """Return the power state of the device.""" state = self.check_power_raw() if state is None: - return {'s1': None, 's2': None, 's3': None, 's4': None} + return {"s1": None, "s2": None, "s3": None, "s4": None} data = {} - data['s1'] = bool(state & 0x01) - data['s2'] = bool(state & 0x02) - data['s3'] = bool(state & 0x04) - data['s4'] = bool(state & 0x08) + data["s1"] = bool(state & 0x01) + data["s2"] = bool(state & 0x02) + data["s3"] = bool(state & 0x04) + data["s4"] = bool(state & 0x08) return data @@ -80,8 +80,8 @@ def get_state(self) -> dict: Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` """ - packet = self._encode(1, b'{}') - response = self.send_packet(0x6a, packet) + packet = self._encode(1, b"{}") + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) return self._decode(response) @@ -98,22 +98,22 @@ def set_state( """Set the power state of the device.""" data = {} if pwr is not None: - data['pwr'] = int(bool(pwr)) + data["pwr"] = int(bool(pwr)) if pwr1 is not None: - data['pwr1'] = int(bool(pwr1)) + data["pwr1"] = int(bool(pwr1)) if pwr2 is not None: - data['pwr2'] = int(bool(pwr2)) + data["pwr2"] = int(bool(pwr2)) if maxworktime is not None: - data['maxworktime'] = maxworktime + data["maxworktime"] = maxworktime if maxworktime1 is not None: - data['maxworktime1'] = maxworktime1 + data["maxworktime1"] = maxworktime1 if maxworktime2 is not None: - data['maxworktime2'] = maxworktime2 + data["maxworktime2"] = maxworktime2 if idcbrightness is not None: - data['idcbrightness'] = idcbrightness - js = json.dumps(data).encode('utf8') + data["idcbrightness"] = idcbrightness + js = json.dumps(data).encode("utf8") packet = self._encode(2, js) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) return self._decode(response) @@ -129,20 +129,22 @@ def _encode(self, flag: int, js: str) -> bytes: # 0x0e- json data packet = bytearray(14) length = 4 + 2 + 2 + 4 + len(js) - struct.pack_into('> 8 return packet def _decode(self, response: bytes) -> dict: """Decode a message.""" payload = self.decrypt(response[0x38:]) - js_len = struct.unpack_from(' None: packet[4] = 3 if state else 2 else: packet[4] = 1 if state else 0 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def set_nightlight(self, state: bool) -> None: @@ -189,14 +191,14 @@ def set_nightlight(self, state: bool) -> None: packet[4] = 3 if state else 1 else: packet[4] = 2 if state else 0 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def check_power(self) -> bool: """Return the power state of the device.""" packet = bytearray(16) packet[0] = 1 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) @@ -205,7 +207,7 @@ def check_nightlight(self) -> bool: """Return the state of the night light.""" packet = bytearray(16) packet[0] = 1 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) @@ -213,14 +215,17 @@ def check_nightlight(self) -> bool: def get_energy(self) -> int: """Return the energy state of the device.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 + return ( + int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + + int(hex(payload[0x05])[2:]) / 100.0 + ) class sp4(device): - """Controls a Broadlink SP4.""" + """Controls a Broadlink SP4.""" def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" @@ -314,7 +319,18 @@ def _encode(self, flag: int, state: dict) -> bytes: payload = json.dumps(state, separators=(",", ":")).encode() packet = bytearray(14) length = 4 + 2 + 2 + 4 + len(payload) - struct.pack_into('=2.1.1'], - description='Python API for controlling Broadlink IR controllers', + install_requires=["cryptography>=2.1.1"], + description="Python API for controlling Broadlink IR controllers", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", ], include_package_data=True, zip_safe=False, From 75e483c9d6f23b09de1f38b86a40f66b85fe0a68 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 7 Nov 2020 05:02:53 -0300 Subject: [PATCH 09/26] Clean up get_energy() (#471) --- broadlink/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/switch.py b/broadlink/switch.py index efea03d7..1a6c6f83 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -216,7 +216,7 @@ def get_energy(self) -> int: response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 + return int((payload[0x07] + payload[0x06] / 100) * 100) + payload[0x05] / 100 class sp4(device): From 5fcea48cbccf8c949e044c128d21d000964c27d3 Mon Sep 17 00:00:00 2001 From: Enosh Date: Tue, 22 Sep 2020 12:26:54 +0300 Subject: [PATCH 10/26] Rebase from main. --- broadlink/__init__.py | 4 +- broadlink/climate.py | 291 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 74ae1fa6..8d4f6c43 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -4,7 +4,7 @@ from typing import Generator, List, Union, Tuple from .alarm import S1C -from .climate import hysen +from .climate import hysen, tornado from .cover import dooya from .device import device, scan from .exceptions import exception @@ -90,12 +90,12 @@ 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), + 0X4E2A: (tornado, "16X SQ", "Tornado"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), } - def gendevice( dev_type: int, host: Tuple[str, int], diff --git a/broadlink/climate.py b/broadlink/climate.py index 036cc9a8..03747e27 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -238,3 +238,294 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: input_payload.append(int(weekend[i]["temp"] * 2)) self.send_request(input_payload) + +class tornado(device): + """Controls a Tornado 16X SQ air conditioner.""" + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) + self.type = "Tornado air conditioner" + + def _decode(self, response) -> bytes: + payload = self.decrypt(bytes(response[0x38:])) + return payload + + def _calculate_checksum(self, packet:bytes, target:int=0x20017) -> tuple: + """Calculate checksum of given array, + by adding little endian words and subtracting from target. + + Args: + packet (list/bytearray/bytes): + """ + result = target - (sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(packet)]) & 0xffff) + return (result & 0xff, (result >> 8) & 0xff) + + def _send_short_payload(self, payload:int) -> bytes: + """Send a request for info from A/C unit and returns the response. + 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO, 3 = unknown function + """ + header = bytearray([0x0c, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00]) + if (payload == 0): + packet = header + bytes([0x21, 0x01, 0x1b, 0x7e]) + elif (payload == 1): + packet = header + bytes([0x11, 0x01, 0x2b, 0x7e]) + elif (payload == 2): + packet = header + bytes([0x41, 0x01, 0xfb, 0x7d]) + elif (payload == 3): + packet = bytearray(16) + packet[0x00] = 0xd0 + packet[0x01] = 0x07 + else: + pass + + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + return (self._decode(response)) + + def get_state(self, payload_debug: bool = None) -> dict: + """Returns a dictionary with the unit's parameters. + + Args: + payload_debug (Optional[bool]): add the received payload for debugging + + Returns: + dict: + state (bool): power + target_temp (float): temperature set point 16> 3) + 8 + (0.0 if ((payload[0xe] & 0b10000000) == 0) else 0.5) + + swing_v = payload[0x0c] & 0b111 + swing_h = (payload[0x0d] & 0b11100000) >> 5 + if (swing_h == 0b111): + data['swing_h'] = 'OFF' + elif (swing_h == 0b000): + data['swing_h'] = 'ON' + else: + data['swing_h'] = 'unrecognized value' + + if (swing_v == 0b111): + data['swing_v'] = 'OFF' + elif (swing_v == 0b000): + data['swing_v'] = 'ON' + elif (swing_v >= 0 and swing_v <=5): + data['swing_v'] = str(swing_v) + else: + data['swing_v'] = 'unrecognized value' + + mode = payload[0x11] >> 3 << 3 + if mode == 0x00: + data['mode'] = 'auto' + elif mode == 0x20: + data['mode'] = 'cooling' + elif mode == 0x40: + data['mode'] = 'drying' + elif mode == 0x80: + data['mode'] = 'heating' + elif mode == 0xc0: + data['mode'] = 'fan' + else: + data['mode'] = 'unrecognized value' + + speed_L = payload[0x0f] + speed_R = payload[0x10] + if speed_L == 0x60 and speed_R == 0x00: + data['speed'] = 'low' + elif speed_L == 0x40 and speed_R == 0x00: + data['speed'] = 'mid' + elif speed_L == 0x20 and speed_R == 0x00: + data['speed'] = 'high' + elif speed_L == 0x40 and speed_R == 0x80: + data['speed'] = 'mute' + elif speed_R == 0x40: + data['speed'] = 'turbo' + elif speed_L == 0xa0 and speed_R == 0x00: + data['speed'] = 'auto' + else: + data['speed'] = 'unrecognized value' + + data['sleep'] = False if (payload[0x11] & 0b100 == 0b000) else True + data['display'] = (payload[0x16] & 0x10 == 0x10) + data['health'] = (payload[0x14] & 0b11 == 0b11) + data['cmnd_0d_rmask'] = payload[0x0d] & 0xf + data['cmnd_0e_rmask'] = payload[0x0e] & 0xf + data['cmnd_18'] = payload[0x18] + + checksum = self._calculate_checksum(payload[:0x19]) # checksum=(payload[0x1a] << 8) + payload[0x19] + + if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): + pass # success + else: + print('checksum fail', ['{:02x}'.format(x) for x in checksum]) + + if (payload_debug): + data['received_payload'] = payload + + return data + + def get_ac_info(self, payload_debug: bool = None) -> dict: + """Returns dictionary with A/C info... + Not implemented yet, except power state. + + Args: + payload_debug (Optional[bool]): add the received payload for debugging + """ + payload = self._send_short_payload(0) + + # first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0 + data = {} + data['state'] = True if (payload[0x0d] & 0b1 == 0b1) else False + + if (payload_debug): + data['received_payload'] = payload + + return data + + def set_advanced(self, + state: bool = None, + mode: str = None, + target_temp: float = None, + speed: str = None, + swing_v: str = None, + swing_h: str = None, + sleep: bool = None, + display: bool = None, + health: bool = None, + cmnd_0d_rmask: int = 0b100, + cmnd_0e_rmask: int = 0b1101, + cmnd_18: int = 0b101, + ) -> bytes: + """Set paramaters of unit and return response. + If not all parameters are specificed, will try to derive from the unit's current state. + + Args: + state (bool): power + target_temp (float): temperature set point 16= 16) and (args['target_temp'] <= 32) and ((args['target_temp'] * 2) % 1 == 0)) + + if (args['swing_v'] == 'OFF'): + swing_L = 0b111 + elif (args['swing_v'] == 'ON'): + swing_L = 0b000 + else: + raise ValueError('unrecognized swing vertical value {}'.format(args['swing_v'])) + + if (args['swing_h'] == 'OFF'): + swing_R = 0b111 + elif (args['swing_h'] == 'ON'): + swing_R = 0b000 + elif (args['swing_h'] >= 0 and args['swing_v'] <= 5): + swing_R = str(args['swing_v']) + else: + raise ValueError('unrecognized swing horizontal value {}'.format(args['swing_h'])) + + if (args['speed'] == 'low'): + speed_L, speed_R = 0x60, 0x00 + elif (args['speed'] == 'mid'): + speed_L, speed_R = 0x40, 0x00 + elif (args['speed'] == 'high'): + speed_L, speed_R = 0x20, 0x00 + elif (args['speed'] == 'mute'): + speed_L, speed_R = 0x40, 0x80 + assert (args['mode'] == 'F') + elif (args['speed'] == 'turbo'): + speed_R = 0x40 + speed_L = 0x20 # doesn't matter + elif (args['speed'] == 'auto'): + speed_L, speed_R = 0xa0, 0x00 + else: + raise ValueError('unrecognized speed value: {}'.format(speed)) + + if (args['mode'] == 'auto'): + mode_1 = 0x00 + elif (args['mode'] == 'cooling'): + mode_1 = 0x20 + elif (args['mode'] == 'drying'): + mode_1 = 0x40 + args['cmnd_0e_rmask'] = 0x16 + elif (args['mode'] == 'heating'): + mode_1 = 0x80 + elif (args['mode'] == 'fan'): + mode_1 = 0xc0 + args['target_temp'] = 24.0 + if args['speed'] == 'turbo': + raise ValueError('speed cannot be {} in fan mode'.format(args['speed'])) + else: + raise ValueError('unrecognized mode value: {}'.format(mode)) + + if (received_state['state'] == True): + if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): + args['checksum_lbit'] = 1 + if (args['swing_h'] == 'ON'): + args['checksum_lbit'] = 0 + + payload[0x0c] = ((int(args['target_temp']) - 8 << 3) | swing_L) + payload[0x0d] = (int(swing_R) << 5) | args['cmnd_0d_rmask'] + payload[0x0e] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0b0) | args['cmnd_0e_rmask'] + payload[0x0f] = speed_L + payload[0x10] = speed_R + payload[0x11] = mode_1 | (0b100 if args['sleep'] else 0b000) + # payload[0x12] = always 0x00 + # payload[0x13] = always 0x00 + payload[0x14] = (0b11 if args['health'] else 0b00) | (0b100000 if args['state'] else 0b000000) + # payload[0x15] = always 0x00 + payload[0x16] = 0b10000 if args['display'] else 0b00000 # 0b_00 also changes + # payload[0x17] = always 0x00 + payload[0x18] = args['cmnd_18'] + + # 0x19-0x1a - checksum + checksum = self._calculate_checksum(payload[:0x19]) # checksum=(payload[0x1a] << 8) + payload[0x19] + payload[0x19] = checksum[0] - args['checksum_lbit'] + payload[0x1a] = checksum[1] + + response = self.send_packet(0x6a, bytearray(payload)) + check_error(response[0x22:0x24]) + response_payload = self._decode(response) + return response_payload From e16fe18dea170b8c0efc8211859e115ca49b9347 Mon Sep 17 00:00:00 2001 From: Enosh Date: Sat, 28 Nov 2020 17:31:29 +0200 Subject: [PATCH 11/26] Simplify `set_advanced` to just one request and create `set_partial` a more user facing/friendly function that doesn't require specifying all the parameters. --- broadlink/__init__.py | 2 +- broadlink/climate.py | 181 +++++++++++++++++++++++++----------------- 2 files changed, 107 insertions(+), 76 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8d4f6c43..af27074a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -90,7 +90,7 @@ 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), - 0X4E2A: (tornado, "16X SQ", "Tornado"), + 0X4E2A: (tornado, "TOP SQ X", "Tornado"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), diff --git a/broadlink/climate.py b/broadlink/climate.py index 03747e27..b0e22532 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -240,7 +240,7 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: self.send_request(input_payload) class tornado(device): - """Controls a Tornado 16X SQ air conditioner.""" + """Controls Tornado TOP SQ X series air conditioners.""" def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "Tornado air conditioner" @@ -275,7 +275,7 @@ def _send_short_payload(self, payload:int) -> bytes: packet[0x00] = 0xd0 packet[0x01] = 0x07 else: - pass + raise ValueError('unrecognized payload type: {}'.format(payload)) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) @@ -303,9 +303,11 @@ def get_state(self, payload_debug: bool = None) -> dict: cmnd_18 (int): unknown """ payload = self._send_short_payload(1) - assert(len(payload) == 32) + if (len(payload) != 32): + raise ValueError('unexpected payload size: {}'.format(len(payload))) + data = {} - data['state'] = True if (payload[0x14] & 0x20) == 0x20 else False + data['state'] = payload[0x14] & 0x20 == 0x20 data['target_temp'] = (payload[0x0c] >> 3) + 8 + (0.0 if ((payload[0xe] & 0b10000000) == 0) else 0.5) swing_v = payload[0x0c] & 0b111 @@ -357,7 +359,7 @@ def get_state(self, payload_debug: bool = None) -> dict: else: data['speed'] = 'unrecognized value' - data['sleep'] = False if (payload[0x11] & 0b100 == 0b000) else True + data['sleep'] = bool(payload[0x11] & 0b100) data['display'] = (payload[0x16] & 0x10 == 0x10) data['health'] = (payload[0x14] & 0b11 == 0b11) data['cmnd_0d_rmask'] = payload[0x0d] & 0xf @@ -395,21 +397,21 @@ def get_ac_info(self, payload_debug: bool = None) -> dict: return data def set_advanced(self, - state: bool = None, - mode: str = None, - target_temp: float = None, - speed: str = None, - swing_v: str = None, - swing_h: str = None, - sleep: bool = None, - display: bool = None, - health: bool = None, - cmnd_0d_rmask: int = 0b100, - cmnd_0e_rmask: int = 0b1101, - cmnd_18: int = 0b101, + state: bool, + mode: str, + target_temp: float, + speed: str, + swing_v: str, + swing_h: str, + sleep: bool, + display: bool, + health: bool, + cmnd_0d_rmask: int, + cmnd_0e_rmask: int, + cmnd_18: int, + checksum_lbit: int ) -> bytes: - """Set paramaters of unit and return response. - If not all parameters are specificed, will try to derive from the unit's current state. + """Set paramaters of unit and return response. All parameters need to be specified. Args: state (bool): power @@ -424,108 +426,137 @@ def set_advanced(self, cmnd_0d_rmask (int): unknown cmnd_0e_rmask (int): unknown cmnd_18 (int): unknown + checksum_lbit (int): subtracted from the left byte of the checksum """ - received_state = self.get_state() - args = { - 'state': state if state != None else received_state['state'], - 'mode': mode if mode != None else received_state['mode'], - 'target_temp': target_temp if target_temp != None else received_state['target_temp'], - 'speed': speed if speed != None else received_state['speed'], - 'swing_v': swing_v if swing_v != None else received_state['swing_v'], - 'swing_h': swing_h if swing_h != None else received_state['swing_h'], - 'sleep': sleep if sleep != None else received_state['sleep'], - 'display': display if display != None else received_state['display'], - 'health': health if health != None else received_state['health'], - 'cmnd_0d_rmask': cmnd_0d_rmask, - 'cmnd_0e_rmask': cmnd_0e_rmask, - 'cmnd_18': cmnd_18, - 'checksum_lbit': 0 - } - PREFIX = [0x19, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x01] # 12B MIDDLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # 13B + 2B checksum SUFFIX = [0, 0, 0, 0, 0] # 5B payload = PREFIX + MIDDLE + SUFFIX - assert ((args['target_temp'] >= 16) and (args['target_temp'] <= 32) and ((args['target_temp'] * 2) % 1 == 0)) + assert ((target_temp >= 16) and (target_temp <= 32) and ((target_temp * 2) % 1 == 0)) - if (args['swing_v'] == 'OFF'): + if (swing_v == 'OFF'): swing_L = 0b111 - elif (args['swing_v'] == 'ON'): + elif (swing_v == 'ON'): swing_L = 0b000 else: - raise ValueError('unrecognized swing vertical value {}'.format(args['swing_v'])) + raise ValueError('unrecognized swing vertical value {}'.format(swing_v)) - if (args['swing_h'] == 'OFF'): + if (swing_h == 'OFF'): swing_R = 0b111 - elif (args['swing_h'] == 'ON'): + elif (swing_h == 'ON'): swing_R = 0b000 - elif (args['swing_h'] >= 0 and args['swing_v'] <= 5): - swing_R = str(args['swing_v']) + elif (swing_h >= 0 and swing_v <= 5): + swing_R = str(swing_v) else: - raise ValueError('unrecognized swing horizontal value {}'.format(args['swing_h'])) + raise ValueError('unrecognized swing horizontal value {}'.format(swing_h)) - if (args['speed'] == 'low'): + if (speed == 'low'): speed_L, speed_R = 0x60, 0x00 - elif (args['speed'] == 'mid'): + elif (speed == 'mid'): speed_L, speed_R = 0x40, 0x00 - elif (args['speed'] == 'high'): + elif (speed == 'high'): speed_L, speed_R = 0x20, 0x00 - elif (args['speed'] == 'mute'): + elif (speed == 'mute'): speed_L, speed_R = 0x40, 0x80 - assert (args['mode'] == 'F') - elif (args['speed'] == 'turbo'): + assert (mode == 'F') + elif (speed == 'turbo'): speed_R = 0x40 speed_L = 0x20 # doesn't matter - elif (args['speed'] == 'auto'): + elif (speed == 'auto'): speed_L, speed_R = 0xa0, 0x00 else: raise ValueError('unrecognized speed value: {}'.format(speed)) - if (args['mode'] == 'auto'): + if (mode == 'auto'): mode_1 = 0x00 - elif (args['mode'] == 'cooling'): + elif (mode == 'cooling'): mode_1 = 0x20 - elif (args['mode'] == 'drying'): + elif (mode == 'drying'): mode_1 = 0x40 - args['cmnd_0e_rmask'] = 0x16 - elif (args['mode'] == 'heating'): + cmnd_0e_rmask = 0x16 + elif (mode == 'heating'): mode_1 = 0x80 - elif (args['mode'] == 'fan'): + elif (mode == 'fan'): mode_1 = 0xc0 - args['target_temp'] = 24.0 - if args['speed'] == 'turbo': - raise ValueError('speed cannot be {} in fan mode'.format(args['speed'])) + target_temp = 24.0 + if speed == 'turbo': + raise ValueError('speed cannot be {} in fan mode'.format(speed)) else: raise ValueError('unrecognized mode value: {}'.format(mode)) - if (received_state['state'] == True): - if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): - args['checksum_lbit'] = 1 - if (args['swing_h'] == 'ON'): - args['checksum_lbit'] = 0 - - payload[0x0c] = ((int(args['target_temp']) - 8 << 3) | swing_L) - payload[0x0d] = (int(swing_R) << 5) | args['cmnd_0d_rmask'] - payload[0x0e] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0b0) | args['cmnd_0e_rmask'] + payload[0x0c] = ((int(target_temp) - 8 << 3) | swing_L) + payload[0x0d] = (int(swing_R) << 5) | cmnd_0d_rmask + payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0) | cmnd_0e_rmask payload[0x0f] = speed_L payload[0x10] = speed_R - payload[0x11] = mode_1 | (0b100 if args['sleep'] else 0b000) + payload[0x11] = mode_1 | (0b100 if sleep else 0b000) # payload[0x12] = always 0x00 # payload[0x13] = always 0x00 - payload[0x14] = (0b11 if args['health'] else 0b00) | (0b100000 if args['state'] else 0b000000) + payload[0x14] = (0b11 if health else 0b00) | (0b100000 if state else 0b000000) # payload[0x15] = always 0x00 - payload[0x16] = 0b10000 if args['display'] else 0b00000 # 0b_00 also changes + payload[0x16] = 0b10000 if display else 0b00000 # 0b_00 also changes # payload[0x17] = always 0x00 - payload[0x18] = args['cmnd_18'] + payload[0x18] = cmnd_18 # 0x19-0x1a - checksum checksum = self._calculate_checksum(payload[:0x19]) # checksum=(payload[0x1a] << 8) + payload[0x19] - payload[0x19] = checksum[0] - args['checksum_lbit'] + payload[0x19] = checksum[0] - checksum_lbit payload[0x1a] = checksum[1] response = self.send_packet(0x6a, bytearray(payload)) check_error(response[0x22:0x24]) response_payload = self._decode(response) return response_payload + + def set_partial(self, + state: bool = None, + mode: str = None, + target_temp: float = None, + speed: str = None, + swing_v: str = None, + swing_h: str = None, + sleep: bool = None, + display: bool = None, + health: bool = None, + cmnd_0d_rmask: int = 0b100, + cmnd_0e_rmask: int = 0b1101, + cmnd_18: int = 0b101, + ) -> bytes: + """Retrieves the current state and changes only the specified parameters. + + Uses `get_state` and `set_advanced` internally.""" + + try: + received_state = self.get_state() + except ValueError as e: + if str(e) == "unexpected payload size: 48": + # Occasionally you will get 48 byte payloads. Reading these isn't implemented yet but a retry should suffice. + received_state = self.get_state() + else: + raise + + args = { + 'state': state if state != None else received_state['state'], + 'mode': mode if mode != None else received_state['mode'], + 'target_temp': target_temp if target_temp != None else received_state['target_temp'], + 'speed': speed if speed != None else received_state['speed'], + 'swing_v': swing_v if swing_v != None else received_state['swing_v'], + 'swing_h': swing_h if swing_h != None else received_state['swing_h'], + 'sleep': sleep if sleep != None else received_state['sleep'], + 'display': display if display != None else received_state['display'], + 'health': health if health != None else received_state['health'], + 'cmnd_0d_rmask': cmnd_0d_rmask, + 'cmnd_0e_rmask': cmnd_0e_rmask, + 'cmnd_18': cmnd_18, + 'checksum_lbit': 0 + } + + if (received_state['state'] == True): + if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): + args['checksum_lbit'] = 1 + if (args['swing_h'] == 'ON'): + args['checksum_lbit'] = 0 + + return self.set_advanced(**args) \ No newline at end of file From cfd2942567fa4dc26cd97fa848c3714a030b0562 Mon Sep 17 00:00:00 2001 From: Enosh Date: Sat, 28 Nov 2020 20:50:25 +0200 Subject: [PATCH 12/26] Fix modes heating,drying,fan when unit is powered off, in `set_partial`. --- broadlink/climate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index b0e22532..f2551df9 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -553,10 +553,9 @@ def set_partial(self, 'checksum_lbit': 0 } - if (received_state['state'] == True): - if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): - args['checksum_lbit'] = 1 - if (args['swing_h'] == 'ON'): - args['checksum_lbit'] = 0 - - return self.set_advanced(**args) \ No newline at end of file + if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): + args['checksum_lbit'] = 1 + if (args['swing_h'] == 'ON'): + args['checksum_lbit'] = 0 + + return self.set_advanced(**args) From ee3bb1f87481a228c8a6cad9eda5c1f1af9f6d0e Mon Sep 17 00:00:00 2001 From: Enosh Date: Sun, 29 Nov 2020 16:41:03 +0200 Subject: [PATCH 13/26] Fix some bugs in set_advanced. --- broadlink/climate.py | 132 ++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 53 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index f2551df9..23882bfe 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -281,11 +281,17 @@ def _send_short_payload(self, payload:int) -> bytes: check_error(response[0x22:0x24]) return (self._decode(response)) - def get_state(self, payload_debug: bool = None) -> dict: + def get_state(self, + payload_debug: bool = False, + unidentified_commands_debug: bool = False, + checksum_debug: bool = False + ) -> dict: """Returns a dictionary with the unit's parameters. Args: payload_debug (Optional[bool]): add the received payload for debugging + unidentified_commands_debug (Optional[bool]): add cmnd_0d_rmask, cmnd_0e_rmask and cmnd_18 for debugging + checksum_debug (Optional[bool]): try to calculate checksum and compare with actual Returns: dict: @@ -298,9 +304,6 @@ def get_state(self, payload_debug: bool = None) -> dict: sleep (bool): display (bool): health (bool): - cmnd_0d_rmask (int): unknown - cmnd_0e_rmask (int): unknown - cmnd_18 (int): unknown """ payload = self._send_short_payload(1) if (len(payload) != 32): @@ -362,23 +365,33 @@ def get_state(self, payload_debug: bool = None) -> dict: data['sleep'] = bool(payload[0x11] & 0b100) data['display'] = (payload[0x16] & 0x10 == 0x10) data['health'] = (payload[0x14] & 0b11 == 0b11) - data['cmnd_0d_rmask'] = payload[0x0d] & 0xf - data['cmnd_0e_rmask'] = payload[0x0e] & 0xf - data['cmnd_18'] = payload[0x18] - checksum = self._calculate_checksum(payload[:0x19]) # checksum=(payload[0x1a] << 8) + payload[0x19] + if (unidentified_commands_debug): + data['cmnd_0d_rmask'] = payload[0x0d] & 0xf + data['cmnd_0e_rmask'] = payload[0x0e] & 0xf + data['cmnd_18'] = payload[0x18] - if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): - pass # success - else: - print('checksum fail', ['{:02x}'.format(x) for x in checksum]) + if (checksum_debug): + checksum = list(self._calculate_checksum(payload[:0x19])) + + # a kludge to make the checksum work + if (data['mode'] == 'heating' or data['mode'] == 'fan' or data['mode'] == 'drying'): + if swing_h == 'OFF': + checksum[0] += 1 + pass + + if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): + pass # success + else: + print('in get_state, checksum fail: calculated {:02x} {:02x}, actual {:02x} {:02x}' \ + .format(checksum[0], checksum[1], payload[0x19], payload[0x1a])) if (payload_debug): data['received_payload'] = payload return data - def get_ac_info(self, payload_debug: bool = None) -> dict: + def get_ac_info(self, payload_debug: bool = False) -> dict: """Returns dictionary with A/C info... Not implemented yet, except power state. @@ -406,10 +419,10 @@ def set_advanced(self, sleep: bool, display: bool, health: bool, - cmnd_0d_rmask: int, - cmnd_0e_rmask: int, - cmnd_18: int, - checksum_lbit: int + cmnd_0d_rmask: int = 0b100, + cmnd_0e_rmask: int = 0b1101, + cmnd_18: int = 0b101, + checksum_lbit: int = None, ) -> bytes: """Set paramaters of unit and return response. All parameters need to be specified. @@ -423,10 +436,10 @@ def set_advanced(self, sleep (bool) display (bool) health (bool) - cmnd_0d_rmask (int): unknown - cmnd_0e_rmask (int): unknown - cmnd_18 (int): unknown - checksum_lbit (int): subtracted from the left byte of the checksum + cmnd_0d_rmask (Optional[int]): override an unidentified command + cmnd_0e_rmask (Optional[int]): override an unidentified command + cmnd_18 (Optional[int]): override an unidentified command + checksum_lbit (Optional[int]): subtracted from the left byte of the checksum """ PREFIX = [0x19, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x01] # 12B @@ -440,6 +453,8 @@ def set_advanced(self, swing_L = 0b111 elif (swing_v == 'ON'): swing_L = 0b000 + elif (int(swing_v) >= 0 and int(swing_v) <= 5): + swing_L = int(swing_v) else: raise ValueError('unrecognized swing vertical value {}'.format(swing_v)) @@ -447,11 +462,23 @@ def set_advanced(self, swing_R = 0b111 elif (swing_h == 'ON'): swing_R = 0b000 - elif (swing_h >= 0 and swing_v <= 5): - swing_R = str(swing_v) else: raise ValueError('unrecognized swing horizontal value {}'.format(swing_h)) + if (mode == 'auto'): + mode_1 = 0x00 + elif (mode == 'cooling'): + mode_1 = 0x20 + elif (mode == 'drying'): + mode_1 = 0x40 + elif (mode == 'heating'): + mode_1 = 0x80 + elif (mode == 'fan'): + mode_1 = 0xc0 + # target_temp is irrelevant in this case + else: + raise ValueError('unrecognized mode value: {}'.format(mode)) + if (speed == 'low'): speed_L, speed_R = 0x60, 0x00 elif (speed == 'mid'): @@ -460,34 +487,31 @@ def set_advanced(self, speed_L, speed_R = 0x20, 0x00 elif (speed == 'mute'): speed_L, speed_R = 0x40, 0x80 - assert (mode == 'F') + if mode != 'fan': + raise ValueError('mute speed is only available in fan mode') elif (speed == 'turbo'): - speed_R = 0x40 speed_L = 0x20 # doesn't matter + speed_R = 0x40 + if not (mode == 'cooling' or mode == 'heating'): + raise ValueError('turbo speed is only available in cooling and heating modes') elif (speed == 'auto'): speed_L, speed_R = 0xa0, 0x00 else: raise ValueError('unrecognized speed value: {}'.format(speed)) - if (mode == 'auto'): - mode_1 = 0x00 - elif (mode == 'cooling'): - mode_1 = 0x20 - elif (mode == 'drying'): - mode_1 = 0x40 - cmnd_0e_rmask = 0x16 - elif (mode == 'heating'): - mode_1 = 0x80 - elif (mode == 'fan'): - mode_1 = 0xc0 - target_temp = 24.0 - if speed == 'turbo': - raise ValueError('speed cannot be {} in fan mode'.format(speed)) + # a kludge to make the checksum work + if checksum_lbit != None: # allow for override + pass + elif (mode == 'heating' or mode == 'fan' or mode == 'drying'): + if (swing_h == 'OFF'): + checksum_lbit = 1 + else: + checksum_lbit = 0 else: - raise ValueError('unrecognized mode value: {}'.format(mode)) + checksum_lbit = 0 payload[0x0c] = ((int(target_temp) - 8 << 3) | swing_L) - payload[0x0d] = (int(swing_R) << 5) | cmnd_0d_rmask + payload[0x0d] = (swing_R << 5) | cmnd_0d_rmask payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0) | cmnd_0e_rmask payload[0x0f] = speed_L payload[0x10] = speed_R @@ -520,9 +544,10 @@ def set_partial(self, sleep: bool = None, display: bool = None, health: bool = None, - cmnd_0d_rmask: int = 0b100, - cmnd_0e_rmask: int = 0b1101, - cmnd_18: int = 0b101, + cmnd_0d_rmask: int = None, + cmnd_0e_rmask: int = None, + cmnd_18: int = None, + checksum_lbit = None ) -> bytes: """Retrieves the current state and changes only the specified parameters. @@ -546,16 +571,17 @@ def set_partial(self, 'swing_h': swing_h if swing_h != None else received_state['swing_h'], 'sleep': sleep if sleep != None else received_state['sleep'], 'display': display if display != None else received_state['display'], - 'health': health if health != None else received_state['health'], - 'cmnd_0d_rmask': cmnd_0d_rmask, - 'cmnd_0e_rmask': cmnd_0e_rmask, - 'cmnd_18': cmnd_18, - 'checksum_lbit': 0 + 'health': health if health != None else received_state['health'] } - if (args['mode'] == 'heating' or args['mode'] == 'fan' or args['mode'] == 'drying'): - args['checksum_lbit'] = 1 - if (args['swing_h'] == 'ON'): - args['checksum_lbit'] = 0 + # Allow overriding of optional parameters + if cmnd_0d_rmask != None: + args['cmnd_0d_rmask'] = cmnd_0d_rmask + if cmnd_0e_rmask != None: + args['cmnd_0e_rmask'] = cmnd_0e_rmask + if cmnd_18 != None: + args['cmnd_18'] = cmnd_18 + if checksum_lbit != None: + args['checksum_lbit'] = checksum_lbit return self.set_advanced(**args) From 991309760e17aafeb21ac31180a4036f7d3158ec Mon Sep 17 00:00:00 2001 From: Enosh Date: Sun, 29 Nov 2020 19:04:20 +0200 Subject: [PATCH 14/26] Better ifs. Fix flake8 issues. --- broadlink/climate.py | 159 +++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 75 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 23882bfe..3032c67f 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -239,27 +239,29 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: self.send_request(input_payload) + class tornado(device): """Controls Tornado TOP SQ X series air conditioners.""" def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "Tornado air conditioner" - + def _decode(self, response) -> bytes: payload = self.decrypt(bytes(response[0x38:])) return payload - - def _calculate_checksum(self, packet:bytes, target:int=0x20017) -> tuple: + + def _calculate_checksum(self, packet: bytes, target: int = 0x20017) -> tuple: """Calculate checksum of given array, by adding little endian words and subtracting from target. - + Args: - packet (list/bytearray/bytes): + packet (bytes): the packet without a checksum + target (int): the sum is subtracted it to create the checksum """ result = target - (sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(packet)]) & 0xffff) return (result & 0xff, (result >> 8) & 0xff) - def _send_short_payload(self, payload:int) -> bytes: + def _send_short_payload(self, payload: int) -> bytes: """Send a request for info from A/C unit and returns the response. 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO, 3 = unknown function """ @@ -281,18 +283,19 @@ def _send_short_payload(self, payload:int) -> bytes: check_error(response[0x22:0x24]) return (self._decode(response)) - def get_state(self, - payload_debug: bool = False, - unidentified_commands_debug: bool = False, - checksum_debug: bool = False - ) -> dict: + def get_state( + self, + payload_debug: bool = False, + unidentified_commands_debug: bool = False, + checksum_debug: bool = False + ) -> dict: """Returns a dictionary with the unit's parameters. - + Args: payload_debug (Optional[bool]): add the received payload for debugging unidentified_commands_debug (Optional[bool]): add cmnd_0d_rmask, cmnd_0e_rmask and cmnd_18 for debugging checksum_debug (Optional[bool]): try to calculate checksum and compare with actual - + Returns: dict: state (bool): power @@ -301,32 +304,33 @@ def get_state(self, speed (str): mute, low, mid, high, turbo (available only in cooling) swing_h (str): ON, OFF swing_v (str): ON, OFF, 1, 2, 3, 4, 5 (fixed positions) - sleep (bool): + sleep (bool): display (bool): health (bool): """ payload = self._send_short_payload(1) if (len(payload) != 32): raise ValueError('unexpected payload size: {}'.format(len(payload))) - + data = {} data['state'] = payload[0x14] & 0x20 == 0x20 - data['target_temp'] = (payload[0x0c] >> 3) + 8 + (0.0 if ((payload[0xe] & 0b10000000) == 0) else 0.5) + data['target_temp'] = (8 + (payload[0x0c] >> 3) + + (0 if ((payload[0xe] & 0b10000000) == 0) else 0.5)) swing_v = payload[0x0c] & 0b111 swing_h = (payload[0x0d] & 0b11100000) >> 5 - if (swing_h == 0b111): + if swing_h == 0b111: data['swing_h'] = 'OFF' - elif (swing_h == 0b000): + elif swing_h == 0b000: data['swing_h'] = 'ON' else: data['swing_h'] = 'unrecognized value' - - if (swing_v == 0b111): + + if swing_v == 0b111: data['swing_v'] = 'OFF' - elif (swing_v == 0b000): + elif swing_v == 0b000: data['swing_v'] = 'ON' - elif (swing_v >= 0 and swing_v <=5): + elif (swing_v >= 0 and swing_v <= 5): data['swing_v'] = str(swing_v) else: data['swing_v'] = 'unrecognized value' @@ -346,7 +350,7 @@ def get_state(self, data['mode'] = 'unrecognized value' speed_L = payload[0x0f] - speed_R = payload[0x10] + speed_R = payload[0x10] if speed_L == 0x60 and speed_R == 0x00: data['speed'] = 'low' elif speed_L == 0x40 and speed_R == 0x00: @@ -375,16 +379,16 @@ def get_state(self, checksum = list(self._calculate_checksum(payload[:0x19])) # a kludge to make the checksum work - if (data['mode'] == 'heating' or data['mode'] == 'fan' or data['mode'] == 'drying'): + if data['mode'] in ('heating', 'drying', 'fan'): if swing_h == 'OFF': checksum[0] += 1 - pass if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): - pass # success + pass # success else: - print('in get_state, checksum fail: calculated {:02x} {:02x}, actual {:02x} {:02x}' \ - .format(checksum[0], checksum[1], payload[0x19], payload[0x1a])) + print('in get_state, checksum fail: \ + calculated {:02x} {:02x}, actual {:02x} {:02x}' + .format(checksum[0], checksum[1], payload[0x19], payload[0x1a])) if (payload_debug): data['received_payload'] = payload @@ -402,14 +406,15 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: # first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0 data = {} - data['state'] = True if (payload[0x0d] & 0b1 == 0b1) else False + data['state'] = payload[0x0d] & 0b1 == 0b1 if (payload_debug): data['received_payload'] = payload return data - def set_advanced(self, + def set_advanced( + self, state: bool, mode: str, target_temp: float, @@ -422,18 +427,18 @@ def set_advanced(self, cmnd_0d_rmask: int = 0b100, cmnd_0e_rmask: int = 0b1101, cmnd_18: int = 0b101, - checksum_lbit: int = None, + checksum_lbit: int = None ) -> bytes: """Set paramaters of unit and return response. All parameters need to be specified. - + Args: state (bool): power target_temp (float): temperature set point 16= 16) and (target_temp <= 32) and ((target_temp * 2) % 1 == 0)) - if (swing_v == 'OFF'): + if swing_v == 'OFF': swing_L = 0b111 - elif (swing_v == 'ON'): + elif swing_v == 'ON': swing_L = 0b000 elif (int(swing_v) >= 0 and int(swing_v) <= 5): swing_L = int(swing_v) else: raise ValueError('unrecognized swing vertical value {}'.format(swing_v)) - if (swing_h == 'OFF'): + if swing_h == 'OFF': swing_R = 0b111 - elif (swing_h == 'ON'): + elif swing_h == 'ON': swing_R = 0b000 else: raise ValueError('unrecognized swing horizontal value {}'.format(swing_h)) @@ -479,38 +486,38 @@ def set_advanced(self, else: raise ValueError('unrecognized mode value: {}'.format(mode)) - if (speed == 'low'): + if speed == 'low': speed_L, speed_R = 0x60, 0x00 - elif (speed == 'mid'): + elif speed == 'mid': speed_L, speed_R = 0x40, 0x00 - elif (speed == 'high'): + elif speed == 'high': speed_L, speed_R = 0x20, 0x00 - elif (speed == 'mute'): + elif speed == 'mute': speed_L, speed_R = 0x40, 0x80 if mode != 'fan': raise ValueError('mute speed is only available in fan mode') - elif (speed == 'turbo'): - speed_L = 0x20 # doesn't matter + elif speed == 'turbo': + speed_L = 0x20 # doesn't matter speed_R = 0x40 - if not (mode == 'cooling' or mode == 'heating'): + if mode not in ('cooling', 'heating'): raise ValueError('turbo speed is only available in cooling and heating modes') - elif (speed == 'auto'): + elif speed == 'auto': speed_L, speed_R = 0xa0, 0x00 else: raise ValueError('unrecognized speed value: {}'.format(speed)) # a kludge to make the checksum work - if checksum_lbit != None: # allow for override + if checksum_lbit is not None: # allow for override pass - elif (mode == 'heating' or mode == 'fan' or mode == 'drying'): - if (swing_h == 'OFF'): + elif mode in ('heating', 'drying', 'fan'): + if swing_h == 'OFF': checksum_lbit = 1 else: checksum_lbit = 0 else: checksum_lbit = 0 - payload[0x0c] = ((int(target_temp) - 8 << 3) | swing_L) + payload[0x0c] = (int(target_temp) - 8 << 3) | swing_L payload[0x0d] = (swing_R << 5) | cmnd_0d_rmask payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0) | cmnd_0e_rmask payload[0x0f] = speed_L @@ -520,12 +527,12 @@ def set_advanced(self, # payload[0x13] = always 0x00 payload[0x14] = (0b11 if health else 0b00) | (0b100000 if state else 0b000000) # payload[0x15] = always 0x00 - payload[0x16] = 0b10000 if display else 0b00000 # 0b_00 also changes + payload[0x16] = 0b10000 if display else 0b00000 # 0b_00 also changes # payload[0x17] = always 0x00 payload[0x18] = cmnd_18 - + # 0x19-0x1a - checksum - checksum = self._calculate_checksum(payload[:0x19]) # checksum=(payload[0x1a] << 8) + payload[0x19] + checksum = self._calculate_checksum(payload[:0x19]) payload[0x19] = checksum[0] - checksum_lbit payload[0x1a] = checksum[1] @@ -534,7 +541,8 @@ def set_advanced(self, response_payload = self._decode(response) return response_payload - def set_partial(self, + def set_partial( + self, state: bool = None, mode: str = None, target_temp: float = None, @@ -547,41 +555,42 @@ def set_partial(self, cmnd_0d_rmask: int = None, cmnd_0e_rmask: int = None, cmnd_18: int = None, - checksum_lbit = None + checksum_lbit: int = None ) -> bytes: """Retrieves the current state and changes only the specified parameters. - + Uses `get_state` and `set_advanced` internally.""" try: received_state = self.get_state() except ValueError as e: if str(e) == "unexpected payload size: 48": - # Occasionally you will get 48 byte payloads. Reading these isn't implemented yet but a retry should suffice. + # Occasionally you will get 48 byte payloads, reading these + # isn't implemented yet but a retry should suffice. received_state = self.get_state() else: raise args = { - 'state': state if state != None else received_state['state'], - 'mode': mode if mode != None else received_state['mode'], - 'target_temp': target_temp if target_temp != None else received_state['target_temp'], - 'speed': speed if speed != None else received_state['speed'], - 'swing_v': swing_v if swing_v != None else received_state['swing_v'], - 'swing_h': swing_h if swing_h != None else received_state['swing_h'], - 'sleep': sleep if sleep != None else received_state['sleep'], - 'display': display if display != None else received_state['display'], - 'health': health if health != None else received_state['health'] + 'state': state if state is not None else received_state['state'], + 'mode': mode if mode is not None else received_state['mode'], + 'target_temp': target_temp if target_temp is not None else received_state['target_temp'], + 'speed': speed if speed is not None else received_state['speed'], + 'swing_v': swing_v if swing_v is not None else received_state['swing_v'], + 'swing_h': swing_h if swing_h is not None else received_state['swing_h'], + 'sleep': sleep if sleep is not None else received_state['sleep'], + 'display': display if display is not None else received_state['display'], + 'health': health if health is not None else received_state['health'] } # Allow overriding of optional parameters - if cmnd_0d_rmask != None: + if cmnd_0d_rmask is not None: args['cmnd_0d_rmask'] = cmnd_0d_rmask - if cmnd_0e_rmask != None: + if cmnd_0e_rmask is not None: args['cmnd_0e_rmask'] = cmnd_0e_rmask - if cmnd_18 != None: + if cmnd_18 is not None: args['cmnd_18'] = cmnd_18 - if checksum_lbit != None: + if checksum_lbit is not None: args['checksum_lbit'] = checksum_lbit - + return self.set_advanced(**args) From e2459644952490e3f737eaf5c4a514ea17398c71 Mon Sep 17 00:00:00 2001 From: Enosh Date: Sun, 29 Nov 2020 22:30:53 +0200 Subject: [PATCH 15/26] Rename `torando` class to `xsq`. --- broadlink/__init__.py | 4 ++-- broadlink/climate.py | 24 ++++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index af27074a..77fcbb01 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -4,7 +4,7 @@ from typing import Generator, List, Union, Tuple from .alarm import S1C -from .climate import hysen, tornado +from .climate import hysen, xsq from .cover import dooya from .device import device, scan from .exceptions import exception @@ -90,7 +90,7 @@ 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), - 0X4E2A: (tornado, "TOP SQ X", "Tornado"), + 0X4E2A: (xsq, "X SQ", "Tornado"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), diff --git a/broadlink/climate.py b/broadlink/climate.py index 3032c67f..80aabde3 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -240,8 +240,8 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: self.send_request(input_payload) -class tornado(device): - """Controls Tornado TOP SQ X series air conditioners.""" +class xsq(device): + """Controls Tornado SMART X SQ series air conditioners.""" def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "Tornado air conditioner" @@ -284,11 +284,11 @@ def _send_short_payload(self, payload: int) -> bytes: return (self._decode(response)) def get_state( - self, - payload_debug: bool = False, - unidentified_commands_debug: bool = False, - checksum_debug: bool = False - ) -> dict: + self, + payload_debug: bool = False, + unidentified_commands_debug: bool = False, + checksum_debug: bool = False + ) -> dict: """Returns a dictionary with the unit's parameters. Args: @@ -315,7 +315,7 @@ def get_state( data = {} data['state'] = payload[0x14] & 0x20 == 0x20 data['target_temp'] = (8 + (payload[0x0c] >> 3) - + (0 if ((payload[0xe] & 0b10000000) == 0) else 0.5)) + + (0.0 if ((payload[0xe] & 0b10000000) == 0) else 0.5)) swing_v = payload[0x0c] & 0b111 swing_h = (payload[0x0d] & 0b11100000) >> 5 @@ -531,7 +531,6 @@ def set_advanced( # payload[0x17] = always 0x00 payload[0x18] = cmnd_18 - # 0x19-0x1a - checksum checksum = self._calculate_checksum(payload[:0x19]) payload[0x19] = checksum[0] - checksum_lbit payload[0x1a] = checksum[1] @@ -539,6 +538,11 @@ def set_advanced( response = self.send_packet(0x6a, bytearray(payload)) check_error(response[0x22:0x24]) response_payload = self._decode(response) + # Response payloads are 16 bytes long. + # The first 12 bytes are always 0e 00 bb 00 07 00 00 00 04 00 01 01, + # the next two should be the checksum of the sent command + # and the last two are the checksum of the response, + # calculated with the target 0x2000d (probably, only minimally tested). return response_payload def set_partial( @@ -559,7 +563,7 @@ def set_partial( ) -> bytes: """Retrieves the current state and changes only the specified parameters. - Uses `get_state` and `set_advanced` internally.""" + Uses `get_state` and `set_advanced` internally (see it for usage).""" try: received_state = self.get_state() From 09527ae016799fe2d90a2460e6daece2c78c8c81 Mon Sep 17 00:00:00 2001 From: Enosh Date: Sun, 29 Nov 2020 22:44:09 +0200 Subject: [PATCH 16/26] Round target_temp. --- broadlink/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 80aabde3..de6721c0 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -454,7 +454,9 @@ def set_advanced( SUFFIX = [0, 0, 0, 0, 0] # 5B payload = PREFIX + MIDDLE + SUFFIX - assert ((target_temp >= 16) and (target_temp <= 32) and ((target_temp * 2) % 1 == 0)) + target_temp = round(target_temp * 2) / 2 + if not (target_temp >= 16 and target_temp <= 32): + raise ValueError('target_temp out of range, value: {}'.format(target_temp)) if swing_v == 'OFF': swing_L = 0b111 From e999a796a11142bbc00c1e1d523d9498a0d8bb77 Mon Sep 17 00:00:00 2001 From: Enosh Date: Tue, 1 Dec 2020 14:37:05 +0200 Subject: [PATCH 17/26] Rename class to `sq1`, add clean and mildew support, use f-strings, _calculate_checksum now returns bytes instead of tuple. `get_ac_info` reads ambient temperature! --- broadlink/__init__.py | 4 +-- broadlink/climate.py | 77 +++++++++++++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 77fcbb01..984e5191 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -4,7 +4,7 @@ from typing import Generator, List, Union, Tuple from .alarm import S1C -from .climate import hysen, xsq +from .climate import hysen, sq1 from .cover import dooya from .device import device, scan from .exceptions import exception @@ -90,7 +90,7 @@ 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), - 0X4E2A: (xsq, "X SQ", "Tornado"), + 0X4E2A: (sq1, "SQ", "Tornado"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), diff --git a/broadlink/climate.py b/broadlink/climate.py index de6721c0..c324f91a 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -240,17 +240,17 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: self.send_request(input_payload) -class xsq(device): +class sq1(device): """Controls Tornado SMART X SQ series air conditioners.""" def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) - self.type = "Tornado air conditioner" + self.type = "Tornado SQ air conditioner" def _decode(self, response) -> bytes: payload = self.decrypt(bytes(response[0x38:])) return payload - def _calculate_checksum(self, packet: bytes, target: int = 0x20017) -> tuple: + def _calculate_checksum(self, packet: bytes, target: int = 0x20017) -> bytes: """Calculate checksum of given array, by adding little endian words and subtracting from target. @@ -259,7 +259,8 @@ def _calculate_checksum(self, packet: bytes, target: int = 0x20017) -> tuple: target (int): the sum is subtracted it to create the checksum """ result = target - (sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(packet)]) & 0xffff) - return (result & 0xff, (result >> 8) & 0xff) + result &= 0xffff + return result.to_bytes(2, 'little') def _send_short_payload(self, payload: int) -> bytes: """Send a request for info from A/C unit and returns the response. @@ -307,6 +308,8 @@ def get_state( sleep (bool): display (bool): health (bool): + clean (bool): + mildew (bool) """ payload = self._send_short_payload(1) if (len(payload) != 32): @@ -367,8 +370,12 @@ def get_state( data['speed'] = 'unrecognized value' data['sleep'] = bool(payload[0x11] & 0b100) - data['display'] = (payload[0x16] & 0x10 == 0x10) - data['health'] = (payload[0x14] & 0b11 == 0b11) + + data['health'] = bool(payload[0x14] & 0b10) + data['clean'] = bool(payload[0x14] & 0b100) + + data['display'] = bool(payload[0x16] & 0b10000) + data['mildew'] = bool(payload[0x16] & 0b1000) if (unidentified_commands_debug): data['cmnd_0d_rmask'] = payload[0x0d] & 0xf @@ -376,7 +383,7 @@ def get_state( data['cmnd_18'] = payload[0x18] if (checksum_debug): - checksum = list(self._calculate_checksum(payload[:0x19])) + checksum = self._calculate_checksum(payload[:0x19]) # a kludge to make the checksum work if data['mode'] in ('heating', 'drying', 'fan'): @@ -386,9 +393,8 @@ def get_state( if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): pass # success else: - print('in get_state, checksum fail: \ - calculated {:02x} {:02x}, actual {:02x} {:02x}' - .format(checksum[0], checksum[1], payload[0x19], payload[0x1a])) + print(f'in get_state, checksum fail: calculated ' + f'{checksum.hex()} actual {payload[0x19:0x1b].hex()}') if (payload_debug): data['received_payload'] = payload @@ -396,18 +402,30 @@ def get_state( return data def get_ac_info(self, payload_debug: bool = False) -> dict: - """Returns dictionary with A/C info... - Not implemented yet, except power state. + """Returns dictionary with A/C info. Args: payload_debug (Optional[bool]): add the received payload for debugging + + Returns: + dict: + state (bool): power + ambient_temp (float): ambient temperature """ payload = self._send_short_payload(0) + if (len(payload) != 48): + raise ValueError(f'get_ac_info, unexpected payload size: {len(payload)}') - # first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0 + # The first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0, + # bytes 0x23,0x24 are the checksum of [:0x22] calculated with target 0x20020. + # bytes 0x25 forward are always empty data = {} data['state'] = payload[0x0d] & 0b1 == 0b1 + ambient_temp = payload[0x11] & 0b00011111 + if ambient_temp: + data['ambient_temp'] = ambient_temp + float(payload[0x21] & 0b00011111) / 10.0 + if (payload_debug): data['received_payload'] = payload @@ -424,6 +442,8 @@ def set_advanced( sleep: bool, display: bool, health: bool, + clean: bool, + mildew: bool, cmnd_0d_rmask: int = 0b100, cmnd_0e_rmask: int = 0b1101, cmnd_18: int = 0b101, @@ -441,9 +461,11 @@ def set_advanced( sleep (bool) display (bool) health (bool) - cmnd_0d_rmask (Optional[int]): override an unidentified command - cmnd_0e_rmask (Optional[int]): override an unidentified command - cmnd_18 (Optional[int]): override an unidentified command + clean (bool) + mildew (bool) + cmnd_0d_rmask (Optional[int]): override an unidentified option + cmnd_0e_rmask (Optional[int]): override an unidentified option + cmnd_18 (Optional[int]): override an unidentified option checksum_lbit (Optional[int]): subtracted from the left byte of the checksum """ @@ -456,7 +478,7 @@ def set_advanced( target_temp = round(target_temp * 2) / 2 if not (target_temp >= 16 and target_temp <= 32): - raise ValueError('target_temp out of range, value: {}'.format(target_temp)) + raise ValueError(f'target_temp out of range, value: {target_temp}') if swing_v == 'OFF': swing_L = 0b111 @@ -465,14 +487,14 @@ def set_advanced( elif (int(swing_v) >= 0 and int(swing_v) <= 5): swing_L = int(swing_v) else: - raise ValueError('unrecognized swing vertical value {}'.format(swing_v)) + raise ValueError(f'unrecognized swing vertical value {swing_v}') if swing_h == 'OFF': swing_R = 0b111 elif swing_h == 'ON': swing_R = 0b000 else: - raise ValueError('unrecognized swing horizontal value {}'.format(swing_h)) + raise ValueError(f'unrecognized swing horizontal value {swing_h}') if (mode == 'auto'): mode_1 = 0x00 @@ -486,7 +508,7 @@ def set_advanced( mode_1 = 0xc0 # target_temp is irrelevant in this case else: - raise ValueError('unrecognized mode value: {}'.format(mode)) + raise ValueError(f'unrecognized mode value {mode}') if speed == 'low': speed_L, speed_R = 0x60, 0x00 @@ -506,7 +528,7 @@ def set_advanced( elif speed == 'auto': speed_L, speed_R = 0xa0, 0x00 else: - raise ValueError('unrecognized speed value: {}'.format(speed)) + raise ValueError(f'unrecognized speed value: {speed}') # a kludge to make the checksum work if checksum_lbit is not None: # allow for override @@ -527,9 +549,12 @@ def set_advanced( payload[0x11] = mode_1 | (0b100 if sleep else 0b000) # payload[0x12] = always 0x00 # payload[0x13] = always 0x00 - payload[0x14] = (0b11 if health else 0b00) | (0b100000 if state else 0b000000) + payload[0x14] = (0b100000 if state else 0b000000 + | 0b100 if clean else 0b000 + | 0b11 if health else 0b00) # payload[0x15] = always 0x00 - payload[0x16] = 0b10000 if display else 0b00000 # 0b_00 also changes + payload[0x16] = (0b10000 if display else 0b00000 + | 0b1000 if mildew else 0b0000) # payload[0x17] = always 0x00 payload[0x18] = cmnd_18 @@ -558,6 +583,8 @@ def set_partial( sleep: bool = None, display: bool = None, health: bool = None, + clean: bool = None, + mildew: bool = None, cmnd_0d_rmask: int = None, cmnd_0e_rmask: int = None, cmnd_18: int = None, @@ -586,7 +613,9 @@ def set_partial( 'swing_h': swing_h if swing_h is not None else received_state['swing_h'], 'sleep': sleep if sleep is not None else received_state['sleep'], 'display': display if display is not None else received_state['display'], - 'health': health if health is not None else received_state['health'] + 'health': health if health is not None else received_state['health'], + 'clean': clean if clean is not None else received_state['clean'], + 'mildew': mildew if mildew is not None else received_state['mildew'] } # Allow overriding of optional parameters From 8f8c11306cb3dae2c0d7e4a2a566c5375cf0c55c Mon Sep 17 00:00:00 2001 From: Enosh Date: Tue, 1 Dec 2020 19:41:12 +0200 Subject: [PATCH 18/26] Handle checksums correctly! --- broadlink/climate.py | 119 +++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index c324f91a..10fcee01 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -250,17 +250,22 @@ def _decode(self, response) -> bytes: payload = self.decrypt(bytes(response[0x38:])) return payload - def _calculate_checksum(self, packet: bytes, target: int = 0x20017) -> bytes: + def _calculate_checksum(self, payload: bytes, byteorder: str) -> bytes: """Calculate checksum of given array, - by adding little endian words and subtracting from target. + by adding little endian words and subtracting from 0xffff. + + The first two bytes of most packets in the class are the length of the + payload and should be cropped out when using this function. Args: - packet (bytes): the packet without a checksum - target (int): the sum is subtracted it to create the checksum + payload (bytes): the payload + byteorder (str): byte order to return the results in """ - result = target - (sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(packet)]) & 0xffff) - result &= 0xffff - return result.to_bytes(2, 'little') + s = sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(payload)]) + # trim the overflow and add it smallest bit + s = (s & 0xffff) + (s >> 16) + result = (0xffff - s) + return result.to_bytes(2, byteorder) def _send_short_payload(self, payload: int) -> bytes: """Send a request for info from A/C unit and returns the response. @@ -278,7 +283,7 @@ def _send_short_payload(self, payload: int) -> bytes: packet[0x00] = 0xd0 packet[0x01] = 0x07 else: - raise ValueError('unrecognized payload type: {}'.format(payload)) + raise ValueError(f'unrecognized payload type: {payload}') response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) @@ -287,15 +292,13 @@ def _send_short_payload(self, payload: int) -> bytes: def get_state( self, payload_debug: bool = False, - unidentified_commands_debug: bool = False, - checksum_debug: bool = False + unidentified_commands_debug: bool = False ) -> dict: """Returns a dictionary with the unit's parameters. Args: - payload_debug (Optional[bool]): add the received payload for debugging + payload_debug (Optional[bool]): prints the received payload unidentified_commands_debug (Optional[bool]): add cmnd_0d_rmask, cmnd_0e_rmask and cmnd_18 for debugging - checksum_debug (Optional[bool]): try to calculate checksum and compare with actual Returns: dict: @@ -313,12 +316,12 @@ def get_state( """ payload = self._send_short_payload(1) if (len(payload) != 32): - raise ValueError('unexpected payload size: {}'.format(len(payload))) + raise ValueError(f'unexpected payload size: {len(payload)}') data = {} data['state'] = payload[0x14] & 0x20 == 0x20 data['target_temp'] = (8 + (payload[0x0c] >> 3) - + (0.0 if ((payload[0xe] & 0b10000000) == 0) else 0.5)) + + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) swing_v = payload[0x0c] & 0b111 swing_h = (payload[0x0d] & 0b11100000) >> 5 @@ -382,22 +385,13 @@ def get_state( data['cmnd_0e_rmask'] = payload[0x0e] & 0xf data['cmnd_18'] = payload[0x18] - if (checksum_debug): - checksum = self._calculate_checksum(payload[:0x19]) - - # a kludge to make the checksum work - if data['mode'] in ('heating', 'drying', 'fan'): - if swing_h == 'OFF': - checksum[0] += 1 - - if (payload[0x19] == checksum[0] and payload[0x1a] == checksum[1]): - pass # success - else: - print(f'in get_state, checksum fail: calculated ' - f'{checksum.hex()} actual {payload[0x19:0x1b].hex()}') + checksum = self._calculate_checksum(payload[2:0x19], 'little') + if payload[0x19:0x1b] != checksum: + print(f'get_state, checksum fail: calculated ' + f'{checksum.hex()} actual {payload[0x19:0x1b].hex()}') if (payload_debug): - data['received_payload'] = payload + print(payload.hex(' ')) return data @@ -405,7 +399,7 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: """Returns dictionary with A/C info. Args: - payload_debug (Optional[bool]): add the received payload for debugging + payload_debug (Optional[bool]): print the received payload Returns: dict: @@ -417,7 +411,7 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: raise ValueError(f'get_ac_info, unexpected payload size: {len(payload)}') # The first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0, - # bytes 0x23,0x24 are the checksum of [:0x22] calculated with target 0x20020. + # bytes 0x23,0x24 are the checksum # bytes 0x25 forward are always empty data = {} data['state'] = payload[0x0d] & 0b1 == 0b1 @@ -426,8 +420,13 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: if ambient_temp: data['ambient_temp'] = ambient_temp + float(payload[0x21] & 0b00011111) / 10.0 + checksum = self._calculate_checksum(payload[2:0x23], 'big') + if (payload[0x23:0x25] != checksum): + print(f'in get_ac_state, checksum fail: calculated ' + f'{checksum.hex()} actual {payload[0x23:0x25].hex()}') + if (payload_debug): - data['received_payload'] = payload + print(payload.hex(' ')) return data @@ -447,9 +446,11 @@ def set_advanced( cmnd_0d_rmask: int = 0b100, cmnd_0e_rmask: int = 0b1101, cmnd_18: int = 0b101, - checksum_lbit: int = None + payload_debug: bool = False ) -> bytes: - """Set paramaters of unit and return response. All parameters need to be specified. + """Set parameters of unit. + + Use `set_partial` to modify only some parameters. Args: state (bool): power @@ -466,7 +467,10 @@ def set_advanced( cmnd_0d_rmask (Optional[int]): override an unidentified option cmnd_0e_rmask (Optional[int]): override an unidentified option cmnd_18 (Optional[int]): override an unidentified option - checksum_lbit (Optional[int]): subtracted from the left byte of the checksum + payload_debug (Optional[bool]): print the constructed payload + + Returns: + True for verified success. """ PREFIX = [0x19, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, @@ -530,20 +534,10 @@ def set_advanced( else: raise ValueError(f'unrecognized speed value: {speed}') - # a kludge to make the checksum work - if checksum_lbit is not None: # allow for override - pass - elif mode in ('heating', 'drying', 'fan'): - if swing_h == 'OFF': - checksum_lbit = 1 - else: - checksum_lbit = 0 - else: - checksum_lbit = 0 - payload[0x0c] = (int(target_temp) - 8 << 3) | swing_L payload[0x0d] = (swing_R << 5) | cmnd_0d_rmask - payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0) | cmnd_0e_rmask + payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0 + | cmnd_0e_rmask) payload[0x0f] = speed_L payload[0x10] = speed_R payload[0x11] = mode_1 | (0b100 if sleep else 0b000) @@ -558,9 +552,11 @@ def set_advanced( # payload[0x17] = always 0x00 payload[0x18] = cmnd_18 - checksum = self._calculate_checksum(payload[:0x19]) - payload[0x19] = checksum[0] - checksum_lbit - payload[0x1a] = checksum[1] + checksum = self._calculate_checksum(payload[2:0x19], 'little') + payload[0x19:0x1b] = checksum + + if (payload_debug): + print(payload.hex(' ')) response = self.send_packet(0x6a, bytearray(payload)) check_error(response[0x22:0x24]) @@ -568,9 +564,13 @@ def set_advanced( # Response payloads are 16 bytes long. # The first 12 bytes are always 0e 00 bb 00 07 00 00 00 04 00 01 01, # the next two should be the checksum of the sent command - # and the last two are the checksum of the response, - # calculated with the target 0x2000d (probably, only minimally tested). - return response_payload + # and the last two are the checksum of the response. + if (response_payload[0xe:0x10] + == self._calculate_checksum(response_payload[2:0xe], 'little')): + if response_payload[0xc:0xe] == checksum: + return True + + return False def set_partial( self, @@ -588,18 +588,18 @@ def set_partial( cmnd_0d_rmask: int = None, cmnd_0e_rmask: int = None, cmnd_18: int = None, - checksum_lbit: int = None - ) -> bytes: + payload_debug: bool = False + ) -> bool: """Retrieves the current state and changes only the specified parameters. - Uses `get_state` and `set_advanced` internally (see it for usage).""" + Uses `get_state` and `set_advanced` internally (see usage there).""" try: received_state = self.get_state() except ValueError as e: if str(e) == "unexpected payload size: 48": - # Occasionally you will get 48 byte payloads, reading these - # isn't implemented yet but a retry should suffice. + # Occasionally a 48 byte payload gets mixed in, + # a retry should suffice. received_state = self.get_state() else: raise @@ -615,7 +615,8 @@ def set_partial( 'display': display if display is not None else received_state['display'], 'health': health if health is not None else received_state['health'], 'clean': clean if clean is not None else received_state['clean'], - 'mildew': mildew if mildew is not None else received_state['mildew'] + 'mildew': mildew if mildew is not None else received_state['mildew'], + 'payload_debug': payload_debug } # Allow overriding of optional parameters @@ -625,7 +626,5 @@ def set_partial( args['cmnd_0e_rmask'] = cmnd_0e_rmask if cmnd_18 is not None: args['cmnd_18'] = cmnd_18 - if checksum_lbit is not None: - args['checksum_lbit'] = checksum_lbit return self.set_advanced(**args) From 692986e2e2fd027e7b38455469e96f07c01149b2 Mon Sep 17 00:00:00 2001 From: Enosh Date: Wed, 9 Dec 2020 15:28:08 +0200 Subject: [PATCH 19/26] Remove set_partial, integate to set_state (renamed from set_advanced), use dictionary as input. Make mute and turbo booleans seperate from speed. Use logging module for debug output. --- broadlink/climate.py | 301 +++++++++++++++++-------------------------- 1 file changed, 121 insertions(+), 180 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 10fcee01..9f316592 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,5 +1,6 @@ """Support for climate control.""" from typing import List +import logging from .device import device from .exceptions import check_error @@ -289,34 +290,28 @@ def _send_short_payload(self, payload: int) -> bytes: check_error(response[0x22:0x24]) return (self._decode(response)) - def get_state( - self, - payload_debug: bool = False, - unidentified_commands_debug: bool = False - ) -> dict: + def get_state(self) -> dict: """Returns a dictionary with the unit's parameters. - Args: - payload_debug (Optional[bool]): prints the received payload - unidentified_commands_debug (Optional[bool]): add cmnd_0d_rmask, cmnd_0e_rmask and cmnd_18 for debugging - Returns: dict: state (bool): power target_temp (float): temperature set point 16> 3 << 3 + mode = payload[0x11] &~ 0b111 # noqa E225 if mode == 0x00: data['mode'] = 'auto' elif mode == 0x20: @@ -355,23 +350,20 @@ def get_state( else: data['mode'] = 'unrecognized value' - speed_L = payload[0x0f] - speed_R = payload[0x10] - if speed_L == 0x60 and speed_R == 0x00: + if payload[0x0f] == 0x60: data['speed'] = 'low' - elif speed_L == 0x40 and speed_R == 0x00: + elif payload[0x0f] == 0x40: data['speed'] = 'mid' - elif speed_L == 0x20 and speed_R == 0x00: + elif payload[0x0f] == 0x20: data['speed'] = 'high' - elif speed_L == 0x40 and speed_R == 0x80: - data['speed'] = 'mute' - elif speed_R == 0x40: - data['speed'] = 'turbo' - elif speed_L == 0xa0 and speed_R == 0x00: + elif payload[0x0f] == 0xa0: data['speed'] = 'auto' else: data['speed'] = 'unrecognized value' + data['mute'] = bool(payload[0x10] == 0x80) + data['turbo'] = bool(payload[0x10] == 0x40) + data['sleep'] = bool(payload[0x11] & 0b100) data['health'] = bool(payload[0x14] & 0b10) @@ -380,27 +372,21 @@ def get_state( data['display'] = bool(payload[0x16] & 0b10000) data['mildew'] = bool(payload[0x16] & 0b1000) - if (unidentified_commands_debug): - data['cmnd_0d_rmask'] = payload[0x0d] & 0xf - data['cmnd_0e_rmask'] = payload[0x0e] & 0xf - data['cmnd_18'] = payload[0x18] - checksum = self._calculate_checksum(payload[2:0x19], 'little') if payload[0x19:0x1b] != checksum: - print(f'get_state, checksum fail: calculated ' - f'{checksum.hex()} actual {payload[0x19:0x1b].hex()}') + logging.warning("checksum fail: calculated %s actual %s", + checksum.hex(), payload[0x19:0x1b].hex()) - if (payload_debug): - print(payload.hex(' ')) + logging.debug("Received payload:\n%s", payload.hex(' ')) + logging.debug("0d[R] mask: %x, 0e[R] mask: %x, cmnd_18: %x", + payload[0x0d] & 0xf, payload[0x0e] & 0xf, payload[0x18]) + logging.debug("Data: %s", data) return data - def get_ac_info(self, payload_debug: bool = False) -> dict: + def get_ac_info(self) -> dict: """Returns dictionary with A/C info. - Args: - payload_debug (Optional[bool]): print the received payload - Returns: dict: state (bool): power @@ -408,7 +394,7 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: """ payload = self._send_short_payload(0) if (len(payload) != 48): - raise ValueError(f'get_ac_info, unexpected payload size: {len(payload)}') + raise ValueError(f"get_ac_info, unexpected payload size: {len(payload)}") # The first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0, # bytes 0x23,0x24 are the checksum @@ -422,145 +408,154 @@ def get_ac_info(self, payload_debug: bool = False) -> dict: checksum = self._calculate_checksum(payload[2:0x23], 'big') if (payload[0x23:0x25] != checksum): - print(f'in get_ac_state, checksum fail: calculated ' - f'{checksum.hex()} actual {payload[0x23:0x25].hex()}') + logging.warning("checksum fail: calculated %s actual %s", + checksum.hex(), payload[0x23:0x25].hex()) - if (payload_debug): - print(payload.hex(' ')) + logging.debug("Received payload:\n%s", payload.hex(' ')) return data - def set_advanced( - self, - state: bool, - mode: str, - target_temp: float, - speed: str, - swing_v: str, - swing_h: str, - sleep: bool, - display: bool, - health: bool, - clean: bool, - mildew: bool, - cmnd_0d_rmask: int = 0b100, - cmnd_0e_rmask: int = 0b1101, - cmnd_18: int = 0b101, - payload_debug: bool = False - ) -> bytes: + def set_state(self, args: dict) -> bytes: """Set parameters of unit. - Use `set_partial` to modify only some parameters. - Args: - state (bool): power - target_temp (float): temperature set point 16 0: + raise ValueError(f"unknown argument(s) {unknown_keys}") + + missing_keys = [key for key in keys if key not in args] + if len(missing_keys) > 0: + try: + received_state = self.get_state() + except RuntimeError as e: + if "unexpected payload size: 48" in str(e): + # Occasionally a 48 byte payload gets mixed in, + # a retry should suffice. + received_state = self.get_state() + else: + raise(e) + logging.debug("raw args %s", args) + received_state.update(args) + args = received_state + logging.debug("filled args %s", args) PREFIX = [0x19, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x01] # 12B MIDDLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # 13B + 2B checksum SUFFIX = [0, 0, 0, 0, 0] # 5B - payload = PREFIX + MIDDLE + SUFFIX + payload = bytearray(PREFIX + MIDDLE + SUFFIX) - target_temp = round(target_temp * 2) / 2 - if not (target_temp >= 16 and target_temp <= 32): - raise ValueError(f'target_temp out of range, value: {target_temp}') + args['target_temp'] = round(args['target_temp'] * 2) / 2 + if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): + raise ValueError(f"target_temp out of range, value: {args['target_temp']}") - if swing_v == 'OFF': + if args['swing_v'] == 'OFF': swing_L = 0b111 - elif swing_v == 'ON': + elif args['swing_v'] == 'ON': swing_L = 0b000 - elif (int(swing_v) >= 0 and int(swing_v) <= 5): - swing_L = int(swing_v) + elif (int(args['swing_v']) >= 0 and int(args['swing_v']) <= 5): + swing_L = int(args['swing_v']) else: - raise ValueError(f'unrecognized swing vertical value {swing_v}') + raise ValueError(f"unrecognized swing vertical value {args['swing_v']}") - if swing_h == 'OFF': + if args['swing_h'] == 'OFF': swing_R = 0b111 - elif swing_h == 'ON': + elif args['swing_h'] == 'ON': swing_R = 0b000 else: - raise ValueError(f'unrecognized swing horizontal value {swing_h}') + raise ValueError(f"unrecognized swing horizontal value {args['swing_h']}") - if (mode == 'auto'): + if (args['mode'] == 'auto'): mode_1 = 0x00 - elif (mode == 'cooling'): + elif (args['mode'] == 'cooling'): mode_1 = 0x20 - elif (mode == 'drying'): + elif (args['mode'] == 'drying'): mode_1 = 0x40 - elif (mode == 'heating'): + elif (args['mode'] == 'heating'): mode_1 = 0x80 - elif (mode == 'fan'): + elif (args['mode'] == 'fan'): mode_1 = 0xc0 # target_temp is irrelevant in this case else: - raise ValueError(f'unrecognized mode value {mode}') - - if speed == 'low': - speed_L, speed_R = 0x60, 0x00 - elif speed == 'mid': - speed_L, speed_R = 0x40, 0x00 - elif speed == 'high': - speed_L, speed_R = 0x20, 0x00 - elif speed == 'mute': - speed_L, speed_R = 0x40, 0x80 - if mode != 'fan': - raise ValueError('mute speed is only available in fan mode') - elif speed == 'turbo': - speed_L = 0x20 # doesn't matter + raise ValueError(f"unrecognized mode value {args['mode']}") + + if args['mute'] and args['turbo']: + raise ValueError("mute and turbo can't be on at once") + elif args['mute']: + speed_R = 0x80 + if args['mode'] != 'fan': + raise ValueError("mute is only available in fan mode") + args['speed'] = 'low' + elif args['turbo']: speed_R = 0x40 - if mode not in ('cooling', 'heating'): - raise ValueError('turbo speed is only available in cooling and heating modes') - elif speed == 'auto': - speed_L, speed_R = 0xa0, 0x00 + if args['mode'] not in ('cooling', 'heating'): + raise ValueError("turbo is only available in cooling/heating") + args['speed'] = 'high' else: - raise ValueError(f'unrecognized speed value: {speed}') + speed_R = 0x00 + + if args['speed'] == 'low': + speed_L = 0x60 + elif args['speed'] == 'mid': + speed_L = 0x40 + elif args['speed'] == 'high': + speed_L = 0x20 + elif args['speed'] == 'auto': + speed_L = 0xa0 + else: + raise ValueError(f"unrecognized speed value: {args['speed']}") - payload[0x0c] = (int(target_temp) - 8 << 3) | swing_L + payload[0x0c] = (int(args['target_temp']) - 8 << 3) | swing_L payload[0x0d] = (swing_R << 5) | cmnd_0d_rmask - payload[0x0e] = (0b10000000 if (target_temp % 1 == 0.5) else 0b0 + payload[0x0e] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 | cmnd_0e_rmask) payload[0x0f] = speed_L payload[0x10] = speed_R - payload[0x11] = mode_1 | (0b100 if sleep else 0b000) + payload[0x11] = mode_1 | (0b100 if args['sleep'] else 0b000) # payload[0x12] = always 0x00 # payload[0x13] = always 0x00 - payload[0x14] = (0b100000 if state else 0b000000 - | 0b100 if clean else 0b000 - | 0b11 if health else 0b00) + payload[0x14] = (0b100000 if args['state'] else 0b000000 + | 0b100 if args['clean'] else 0b000 + | 0b11 if args['health'] else 0b00) # payload[0x15] = always 0x00 - payload[0x16] = (0b10000 if display else 0b00000 - | 0b1000 if mildew else 0b0000) + payload[0x16] = (0b10000 if args['display'] else 0b00000 + | 0b1000 if args['mildew'] else 0b0000) # payload[0x17] = always 0x00 payload[0x18] = cmnd_18 checksum = self._calculate_checksum(payload[2:0x19], 'little') payload[0x19:0x1b] = checksum - if (payload_debug): - print(payload.hex(' ')) + logging.debug("Received payload:\n%s", payload.hex(' ')) response = self.send_packet(0x6a, bytearray(payload)) check_error(response[0x22:0x24]) response_payload = self._decode(response) + logging.debug("Response payload:\n%s", response_payload.hex(' ')) # Response payloads are 16 bytes long. # The first 12 bytes are always 0e 00 bb 00 07 00 00 00 04 00 01 01, # the next two should be the checksum of the sent command @@ -569,62 +564,8 @@ def set_advanced( == self._calculate_checksum(response_payload[2:0xe], 'little')): if response_payload[0xc:0xe] == checksum: return True + else: + logging.warning("Checksum in response %s different from sent payload %s", + response_payload[0xc:0xe].hex(), checksum.hex()) return False - - def set_partial( - self, - state: bool = None, - mode: str = None, - target_temp: float = None, - speed: str = None, - swing_v: str = None, - swing_h: str = None, - sleep: bool = None, - display: bool = None, - health: bool = None, - clean: bool = None, - mildew: bool = None, - cmnd_0d_rmask: int = None, - cmnd_0e_rmask: int = None, - cmnd_18: int = None, - payload_debug: bool = False - ) -> bool: - """Retrieves the current state and changes only the specified parameters. - - Uses `get_state` and `set_advanced` internally (see usage there).""" - - try: - received_state = self.get_state() - except ValueError as e: - if str(e) == "unexpected payload size: 48": - # Occasionally a 48 byte payload gets mixed in, - # a retry should suffice. - received_state = self.get_state() - else: - raise - - args = { - 'state': state if state is not None else received_state['state'], - 'mode': mode if mode is not None else received_state['mode'], - 'target_temp': target_temp if target_temp is not None else received_state['target_temp'], - 'speed': speed if speed is not None else received_state['speed'], - 'swing_v': swing_v if swing_v is not None else received_state['swing_v'], - 'swing_h': swing_h if swing_h is not None else received_state['swing_h'], - 'sleep': sleep if sleep is not None else received_state['sleep'], - 'display': display if display is not None else received_state['display'], - 'health': health if health is not None else received_state['health'], - 'clean': clean if clean is not None else received_state['clean'], - 'mildew': mildew if mildew is not None else received_state['mildew'], - 'payload_debug': payload_debug - } - - # Allow overriding of optional parameters - if cmnd_0d_rmask is not None: - args['cmnd_0d_rmask'] = cmnd_0d_rmask - if cmnd_0e_rmask is not None: - args['cmnd_0e_rmask'] = cmnd_0e_rmask - if cmnd_18 is not None: - args['cmnd_18'] = cmnd_18 - - return self.set_advanced(**args) From db50704d706a9ed76d1598350b3d614be965feef Mon Sep 17 00:00:00 2001 From: Enosh Date: Wed, 9 Dec 2020 16:17:45 +0200 Subject: [PATCH 20/26] Implement an encode func. --- broadlink/climate.py | 98 ++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 9f316592..8937487d 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -268,25 +268,40 @@ def _calculate_checksum(self, payload: bytes, byteorder: str) -> bytes: result = (0xffff - s) return result.to_bytes(2, byteorder) + def _encode(self, payload: bytes) -> bytes: + """Encode payload i.e. add length to beginning, checksum after payload, + and pad size. + """ + import struct + length = 2 + len(payload) # length prefix + packet_length = ((length - 1) // 16 + 1) * 16 + packet = bytearray(2) + struct.pack_into(" bytes: - """Send a request for info from A/C unit and returns the response. - 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO, 3 = unknown function + """Send a request for info from AC unit and returns the response. + 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO """ - header = bytearray([0x0c, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00]) + packet = bytearray([0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00]) if (payload == 0): - packet = header + bytes([0x21, 0x01, 0x1b, 0x7e]) + packet.extend([0x21, 0x01]) elif (payload == 1): - packet = header + bytes([0x11, 0x01, 0x2b, 0x7e]) + packet.extend([0x11, 0x01]) elif (payload == 2): - packet = header + bytes([0x41, 0x01, 0xfb, 0x7d]) - elif (payload == 3): - packet = bytearray(16) - packet[0x00] = 0xd0 - packet[0x01] = 0x07 + packet.extend([0x41, 0x01]) + # elif (payload == 3): + # packet = bytearray(16) + # packet[0x00] = 0xd0 + # packet[0x01] = 0x07 else: - raise ValueError(f'unrecognized payload type: {payload}') + raise ValueError(f"unrecognized payload type: {payload}") - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6a, self._encode(packet)) check_error(response[0x22:0x24]) return (self._decode(response)) @@ -311,7 +326,7 @@ def get_state(self) -> dict: """ payload = self._send_short_payload(1) if (len(payload) != 32): - raise RuntimeError(f'unexpected payload size: {len(payload)}') + raise RuntimeError(f"unexpected payload size: {len(payload)}") data = {} data['state'] = payload[0x14] & 0x20 == 0x20 @@ -378,7 +393,7 @@ def get_state(self) -> dict: checksum.hex(), payload[0x19:0x1b].hex()) logging.debug("Received payload:\n%s", payload.hex(' ')) - logging.debug("0d[R] mask: %x, 0e[R] mask: %x, cmnd_18: %x", + logging.debug("0b[R] mask: %x, 0c[R] mask: %x, cmnd_16: %x", payload[0x0d] & 0xf, payload[0x0e] & 0xf, payload[0x18]) logging.debug("Data: %s", data) @@ -437,11 +452,13 @@ def set_state(self, args: dict) -> bytes: Returns: True for success, verified by the unit's response. """ - cmnd_0d_rmask = 0b100 - cmnd_0e_rmask = 0b1101 - cmnd_18 = 0b101 + cmnd_0b_rmask = 0b100 + cmnd_0c_rmask = 0b1101 + cmnd_16 = 0b101 - keys = ['state', 'mode', 'target_temp', 'speed', 'swing_v', 'swing_h', 'sleep', 'display', 'health', 'clean', 'mildew'] + keys = ['state', 'mode', 'target_temp', 'speed', 'mute', 'turbo', + 'swing_v', 'swing_h', 'sleep', 'display', 'health', 'clean', + 'mildew'] unknown_keys = [key for key in args.keys() if key not in keys] if len(unknown_keys) > 0: raise ValueError(f"unknown argument(s) {unknown_keys}") @@ -462,12 +479,11 @@ def set_state(self, args: dict) -> bytes: args = received_state logging.debug("filled args %s", args) - PREFIX = [0x19, 0x00, 0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, - 0x01, 0x01] # 12B + PREFIX = [0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, + 0x01, 0x01] # 10B MIDDLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0] # 13B + 2B checksum - SUFFIX = [0, 0, 0, 0, 0] # 5B - payload = bytearray(PREFIX + MIDDLE + SUFFIX) + 0, 0, 0] # 13B + payload = bytearray(PREFIX + MIDDLE) args['target_temp'] = round(args['target_temp'] * 2) / 2 if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): @@ -529,30 +545,30 @@ def set_state(self, args: dict) -> bytes: else: raise ValueError(f"unrecognized speed value: {args['speed']}") - payload[0x0c] = (int(args['target_temp']) - 8 << 3) | swing_L - payload[0x0d] = (swing_R << 5) | cmnd_0d_rmask - payload[0x0e] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 - | cmnd_0e_rmask) - payload[0x0f] = speed_L - payload[0x10] = speed_R - payload[0x11] = mode_1 | (0b100 if args['sleep'] else 0b000) - # payload[0x12] = always 0x00 - # payload[0x13] = always 0x00 - payload[0x14] = (0b100000 if args['state'] else 0b000000 + payload[0x0a] = (int(args['target_temp']) - 8 << 3) | swing_L + payload[0x0b] = (swing_R << 5) | cmnd_0b_rmask + payload[0x0c] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 + | cmnd_0c_rmask) + payload[0x0d] = speed_L + payload[0x0e] = speed_R + payload[0x0f] = mode_1 | (0b100 if args['sleep'] else 0b000) + # payload[0x10] = always 0x00 + # payload[0x11] = always 0x00 + payload[0x12] = (0b100000 if args['state'] else 0b000000 | 0b100 if args['clean'] else 0b000 | 0b11 if args['health'] else 0b00) - # payload[0x15] = always 0x00 - payload[0x16] = (0b10000 if args['display'] else 0b00000 + # payload[0x13] = always 0x00 + payload[0x14] = (0b10000 if args['display'] else 0b00000 | 0b1000 if args['mildew'] else 0b0000) - # payload[0x17] = always 0x00 - payload[0x18] = cmnd_18 + # payload[0x15] = always 0x00 + payload[0x16] = cmnd_16 - checksum = self._calculate_checksum(payload[2:0x19], 'little') - payload[0x19:0x1b] = checksum + checksum = self._calculate_checksum(payload, 'little') + payload = self._encode(payload) - logging.debug("Received payload:\n%s", payload.hex(' ')) + logging.debug("Constructed payload:\n%s", payload.hex(' ')) - response = self.send_packet(0x6a, bytearray(payload)) + response = self.send_packet(0x6a, payload) check_error(response[0x22:0x24]) response_payload = self._decode(response) logging.debug("Response payload:\n%s", response_payload.hex(' ')) From e6d151a363ca3c29dd377166966f2cd9ca97f6bf Mon Sep 17 00:00:00 2001 From: Enosh Date: Wed, 9 Dec 2020 17:02:55 +0200 Subject: [PATCH 21/26] replace some complicated ifs with {}.get --- broadlink/climate.py | 75 ++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 8937487d..4962339d 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -333,48 +333,35 @@ def get_state(self) -> dict: data['target_temp'] = (8 + (payload[0x0c] >> 3) + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) - swing_v = payload[0x0c] & 0b111 - swing_h = (payload[0x0d] & 0b11100000) >> 5 - if swing_h == 0b111: - data['swing_h'] = 'OFF' - elif swing_h == 0b000: - data['swing_h'] = 'ON' - else: - data['swing_h'] = 'unrecognized value' - - if swing_v == 0b111: - data['swing_v'] = 'OFF' - elif swing_v == 0b000: - data['swing_v'] = 'ON' - elif (swing_v >= 0 and swing_v <= 5): - data['swing_v'] = str(swing_v) - else: - data['swing_v'] = 'unrecognized value' - - mode = payload[0x11] &~ 0b111 # noqa E225 - if mode == 0x00: - data['mode'] = 'auto' - elif mode == 0x20: - data['mode'] = 'cooling' - elif mode == 0x40: - data['mode'] = 'drying' - elif mode == 0x80: - data['mode'] = 'heating' - elif mode == 0xc0: - data['mode'] = 'fan' - else: - data['mode'] = 'unrecognized value' - - if payload[0x0f] == 0x60: - data['speed'] = 'low' - elif payload[0x0f] == 0x40: - data['speed'] = 'mid' - elif payload[0x0f] == 0x20: - data['speed'] = 'high' - elif payload[0x0f] == 0xa0: - data['speed'] = 'auto' - else: - data['speed'] = 'unrecognized value' + data['swing_h'] = { + 0b000: 'ON', + 0b111: 'OFF' + }.get((payload[0x0d] & 0b11100000) >> 5, 'unrecognized value') + + data['swing_v'] = { + 0b000: 'ON', + 1: '1', + 2: '2', + 3: '3', + 4: '4', + 5: '5', + 0b111: 'OFF' + }.get(payload[0x0c] & 0b111, 'unrecognized value') + + data['mode'] = { + 0x00: 'auto', + 0x20: 'cooling', + 0x40: 'drying', + 0x80: 'heating', + 0xc0: 'fan', + }.get(payload[0x11] &~ 0b111, 'unrecognized value') # noqa E225 + + data['speed'] = { + 0x20: 'high', + 0x40: 'mid', + 0x60: 'low', + 0xa0: 'auto' + }.get(payload[0x0f], 'unrecognized value') data['mute'] = bool(payload[0x10] == 0x80) data['turbo'] = bool(payload[0x10] == 0x40) @@ -400,7 +387,7 @@ def get_state(self) -> dict: return data def get_ac_info(self) -> dict: - """Returns dictionary with A/C info. + """Returns dictionary with AC info. Returns: dict: @@ -473,7 +460,7 @@ def set_state(self, args: dict) -> bytes: # a retry should suffice. received_state = self.get_state() else: - raise(e) + raise e logging.debug("raw args %s", args) received_state.update(args) args = received_state From 573de7331b616cf6f4ab375559781efdb7c7aa12 Mon Sep 17 00:00:00 2001 From: Enosh Date: Wed, 9 Dec 2020 17:33:21 +0200 Subject: [PATCH 22/26] avoid struct in encode, more dict use instead of ifs --- broadlink/climate.py | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 4962339d..2a4807df 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -272,11 +272,9 @@ def _encode(self, payload: bytes) -> bytes: """Encode payload i.e. add length to beginning, checksum after payload, and pad size. """ - import struct length = 2 + len(payload) # length prefix packet_length = ((length - 1) // 16 + 1) * 16 - packet = bytearray(2) - struct.pack_into(" dict: 0x20: 'cooling', 0x40: 'drying', 0x80: 'heating', - 0xc0: 'fan', + 0xc0: 'fan' }.get(payload[0x11] &~ 0b111, 'unrecognized value') # noqa E225 data['speed'] = { @@ -492,18 +490,15 @@ def set_state(self, args: dict) -> bytes: else: raise ValueError(f"unrecognized swing horizontal value {args['swing_h']}") - if (args['mode'] == 'auto'): - mode_1 = 0x00 - elif (args['mode'] == 'cooling'): - mode_1 = 0x20 - elif (args['mode'] == 'drying'): - mode_1 = 0x40 - elif (args['mode'] == 'heating'): - mode_1 = 0x80 - elif (args['mode'] == 'fan'): - mode_1 = 0xc0 - # target_temp is irrelevant in this case - else: + try: + mode_1 = { + 'auto': 0x00, + 'cooling': 0x20, + 'drying': 0x40, + 'heating': 0x80, + 'fan': 0xc0 + }[args['mode']] + except KeyError: raise ValueError(f"unrecognized mode value {args['mode']}") if args['mute'] and args['turbo']: @@ -521,15 +516,14 @@ def set_state(self, args: dict) -> bytes: else: speed_R = 0x00 - if args['speed'] == 'low': - speed_L = 0x60 - elif args['speed'] == 'mid': - speed_L = 0x40 - elif args['speed'] == 'high': - speed_L = 0x20 - elif args['speed'] == 'auto': - speed_L = 0xa0 - else: + try: + speed_L = { + 'high': 0x20, + 'mid': 0x40, + 'low': 0x60, + 'auto': 0xa0 + }[args['speed']] + except KeyError: raise ValueError(f"unrecognized speed value: {args['speed']}") payload[0x0a] = (int(args['target_temp']) - 8 << 3) | swing_L From e4595815bd5dc48fb9edf0275868782de0d5ef3f Mon Sep 17 00:00:00 2001 From: Enosh Date: Wed, 9 Dec 2020 17:53:50 +0200 Subject: [PATCH 23/26] typos, flake8 issues --- broadlink/climate.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 2a4807df..ef504db7 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -263,7 +263,7 @@ def _calculate_checksum(self, payload: bytes, byteorder: str) -> bytes: byteorder (str): byte order to return the results in """ s = sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(payload)]) - # trim the overflow and add it smallest bit + # trim the overflow and add it to smallest bit s = (s & 0xffff) + (s >> 16) result = (0xffff - s) return result.to_bytes(2, byteorder) @@ -329,7 +329,7 @@ def get_state(self) -> dict: data = {} data['state'] = payload[0x14] & 0x20 == 0x20 data['target_temp'] = (8 + (payload[0x0c] >> 3) - + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) + + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) # noqa E501 data['swing_h'] = { 0b000: 'ON', @@ -394,17 +394,18 @@ def get_ac_info(self) -> dict: """ payload = self._send_short_payload(0) if (len(payload) != 48): - raise ValueError(f"get_ac_info, unexpected payload size: {len(payload)}") + raise ValueError(f"unexpected payload size: {len(payload)}") - # The first 13 bytes are the same: 22 00 bb 00 07 00 00 00 18 00 01 21 c0, - # bytes 0x23,0x24 are the checksum - # bytes 0x25 forward are always empty + # Length is 34 (0x22), the next 11 bytes are + # the same: bb 00 07 00 00 00 18 00 01 21 c0, + # bytes 0x23,0x24 are the checksum. data = {} data['state'] = payload[0x0d] & 0b1 == 0b1 ambient_temp = payload[0x11] & 0b00011111 if ambient_temp: - data['ambient_temp'] = ambient_temp + float(payload[0x21] & 0b00011111) / 10.0 + data['ambient_temp'] = (ambient_temp + + float(payload[0x21] & 0b00011111) / 10.0) checksum = self._calculate_checksum(payload[2:0x23], 'big') if (payload[0x23:0x25] != checksum): @@ -419,7 +420,7 @@ def set_state(self, args: dict) -> bytes: """Set parameters of unit. Args: - args: if any are missing the current state will be retrived with `get_state` + args (dict): if any are missing the current value will be retrived state (bool): power target_temp (float): temperature set point 16 bytes: args['target_temp'] = round(args['target_temp'] * 2) / 2 if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): - raise ValueError(f"target_temp out of range, value: {args['target_temp']}") + raise ValueError(f"target_temp out of range, value: {args['target_temp']}") # noqa E501 if args['swing_v'] == 'OFF': swing_L = 0b111 @@ -481,14 +482,14 @@ def set_state(self, args: dict) -> bytes: elif (int(args['swing_v']) >= 0 and int(args['swing_v']) <= 5): swing_L = int(args['swing_v']) else: - raise ValueError(f"unrecognized swing vertical value {args['swing_v']}") + raise ValueError(f"unrecognized swing vertical value {args['swing_v']}") # noqa E501 if args['swing_h'] == 'OFF': swing_R = 0b111 elif args['swing_h'] == 'ON': swing_R = 0b000 else: - raise ValueError(f"unrecognized swing horizontal value {args['swing_h']}") + raise ValueError(f"unrecognized swing horizontal value {args['swing_h']}") # noqa E501 try: mode_1 = { @@ -558,11 +559,13 @@ def set_state(self, args: dict) -> bytes: # the next two should be the checksum of the sent command # and the last two are the checksum of the response. if (response_payload[0xe:0x10] - == self._calculate_checksum(response_payload[2:0xe], 'little')): + == self._calculate_checksum(response_payload[2:0xe], 'little')): # noqa E501 if response_payload[0xc:0xe] == checksum: return True else: - logging.warning("Checksum in response %s different from sent payload %s", - response_payload[0xc:0xe].hex(), checksum.hex()) + logging.warning( + "Checksum in response %s different from sent payload %s", + response_payload[0xc:0xe].hex(), checksum.hex() + ) return False From d0899e710d452aecb18528f95faa3008efff5939 Mon Sep 17 00:00:00 2001 From: Enosh Date: Thu, 10 Dec 2020 16:41:42 +0200 Subject: [PATCH 24/26] First attempt at IntEmun for mode, speed and swing. Simplify _encode. --- broadlink/climate.py | 262 ++++++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 140 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index ef504db7..25f3331d 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,6 +1,7 @@ """Support for climate control.""" from typing import List import logging +from enum import IntEnum, unique from .device import device from .exceptions import check_error @@ -243,6 +244,40 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: class sq1(device): """Controls Tornado SMART X SQ series air conditioners.""" + + REQUEST_PREFIX = bytes([0xbb, 0x00, 0x06, 0x80]) + RESPONSE_PREFIX = bytes([0xbb, 0x00, 0x07, 0x00]) + + @unique + class Mode(IntEnum): + AUTO = 0 + COOLING = 0x20 + DRYING = 0x40 + HEATING = 0x80 + FAN = 0xc0 + + @unique + class Speed(IntEnum): + HIGH = 0x20 + MID = 0x40 + LOW = 0x60 + AUTO = 0xa0 + + @unique + class Swing_H(IntEnum): + ON = 0b000, + OFF = 0b111 + + @unique + class Swing_V(IntEnum): + ON = 0b000, + POS_1 = 1 + POS_2 = 2 + POS_3 = 3 + POS_4 = 4 + POS_5 = 5 + OFF = 0b111 + def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "Tornado SQ air conditioner" @@ -251,7 +286,7 @@ def _decode(self, response) -> bytes: payload = self.decrypt(bytes(response[0x38:])) return payload - def _calculate_checksum(self, payload: bytes, byteorder: str) -> bytes: + def _calculate_checksum(self, payload: bytes) -> int: """Calculate checksum of given array, by adding little endian words and subtracting from 0xffff. @@ -260,44 +295,30 @@ def _calculate_checksum(self, payload: bytes, byteorder: str) -> bytes: Args: payload (bytes): the payload - byteorder (str): byte order to return the results in """ s = sum([v if i % 2 == 0 else v << 8 for i, v in enumerate(payload)]) # trim the overflow and add it to smallest bit s = (s & 0xffff) + (s >> 16) - result = (0xffff - s) - return result.to_bytes(2, byteorder) + return (0xffff - s) def _encode(self, payload: bytes) -> bytes: - """Encode payload i.e. add length to beginning, checksum after payload, - and pad size. - """ - length = 2 + len(payload) # length prefix - packet_length = ((length - 1) // 16 + 1) * 16 - packet = bytearray([length, 0]) - packet.extend(payload) - packet.extend(self._calculate_checksum(packet[2:], 'little')) - packet.extend([0] * (packet_length - len(packet))) # round size to 16 - - return packet + """Encode payload (add length to beginning, checksum after payload).""" + payload = self.REQUEST_PREFIX + payload + checksum = self._calculate_checksum(payload).to_bytes(2, 'little') + return (len(payload) + 2).to_bytes(2, 'little') + payload + checksum def _send_short_payload(self, payload: int) -> bytes: """Send a request for info from AC unit and returns the response. 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO """ - packet = bytearray([0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00]) - if (payload == 0): - packet.extend([0x21, 0x01]) - elif (payload == 1): - packet.extend([0x11, 0x01]) - elif (payload == 2): - packet.extend([0x41, 0x01]) - # elif (payload == 3): - # packet = bytearray(16) - # packet[0x00] = 0xd0 - # packet[0x01] = 0x07 - else: - raise ValueError(f"unrecognized payload type: {payload}") + packet = bytes( + [0x00, 0x00, 0x02, 0x00] + + { + 0: [0x21, 0x01], + 1: [0x11, 0x01], + 2: [0x41, 0x01] + }[payload] + ) response = self.send_packet(0x6a, self._encode(packet)) check_error(response[0x22:0x24]) @@ -310,12 +331,12 @@ def get_state(self) -> dict: dict: state (bool): power target_temp (float): temperature set point 16 dict: data['target_temp'] = (8 + (payload[0x0c] >> 3) + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) # noqa E501 - data['swing_h'] = { - 0b000: 'ON', - 0b111: 'OFF' - }.get((payload[0x0d] & 0b11100000) >> 5, 'unrecognized value') - - data['swing_v'] = { - 0b000: 'ON', - 1: '1', - 2: '2', - 3: '3', - 4: '4', - 5: '5', - 0b111: 'OFF' - }.get(payload[0x0c] & 0b111, 'unrecognized value') - - data['mode'] = { - 0x00: 'auto', - 0x20: 'cooling', - 0x40: 'drying', - 0x80: 'heating', - 0xc0: 'fan' - }.get(payload[0x11] &~ 0b111, 'unrecognized value') # noqa E225 - - data['speed'] = { - 0x20: 'high', - 0x40: 'mid', - 0x60: 'low', - 0xa0: 'auto' - }.get(payload[0x0f], 'unrecognized value') + data['swing_h'] = self.Swing_H((payload[0x0d] & 0b11100000) >> 5) + data['swing_v'] = self.Swing_V(payload[0x0c] & 0b111) + + data['mode'] = self.Mode(payload[0x11] & ~ 0b111) + + data['speed'] = self.Speed(payload[0x0f]) data['mute'] = bool(payload[0x10] == 0x80) data['turbo'] = bool(payload[0x10] == 0x40) @@ -372,7 +370,8 @@ def get_state(self) -> dict: data['display'] = bool(payload[0x16] & 0b10000) data['mildew'] = bool(payload[0x16] & 0b1000) - checksum = self._calculate_checksum(payload[2:0x19], 'little') + checksum = self._calculate_checksum(payload[2:0x19] + ).to_bytes(2, 'little') if payload[0x19:0x1b] != checksum: logging.warning("checksum fail: calculated %s actual %s", checksum.hex(), payload[0x19:0x1b].hex()) @@ -407,7 +406,7 @@ def get_ac_info(self) -> dict: data['ambient_temp'] = (ambient_temp + float(payload[0x21] & 0b00011111) / 10.0) - checksum = self._calculate_checksum(payload[2:0x23], 'big') + checksum = self._calculate_checksum(payload[2:0x23]).to_bytes(2, 'big') if (payload[0x23:0x25] != checksum): logging.warning("checksum fail: calculated %s actual %s", checksum.hex(), payload[0x23:0x25].hex()) @@ -416,19 +415,19 @@ def get_ac_info(self) -> dict: return data - def set_state(self, args: dict) -> bytes: + def set_state(self, args: dict) -> bool: """Set parameters of unit. Args: args (dict): if any are missing the current value will be retrived state (bool): power target_temp (float): temperature set point 16 bytes: Returns: True for success, verified by the unit's response. """ - cmnd_0b_rmask = 0b100 - cmnd_0c_rmask = 0b1101 - cmnd_16 = 0b101 + CMND_0B_RMASK = 0b100 + CMND_0C_RMASK = 0b1101 + CMND_16 = 0b101 keys = ['state', 'mode', 'target_temp', 'speed', 'mute', 'turbo', 'swing_v', 'swing_h', 'sleep', 'display', 'health', 'clean', @@ -460,47 +459,28 @@ def set_state(self, args: dict) -> bytes: received_state = self.get_state() else: raise e - logging.debug("raw args %s", args) + logging.debug("Raw args %s", args) received_state.update(args) args = received_state - logging.debug("filled args %s", args) - - PREFIX = [0xbb, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0f, 0x00, - 0x01, 0x01] # 10B - MIDDLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0] # 13B - payload = bytearray(PREFIX + MIDDLE) + logging.debug("Filled args %s", args) args['target_temp'] = round(args['target_temp'] * 2) / 2 if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): raise ValueError(f"target_temp out of range, value: {args['target_temp']}") # noqa E501 - if args['swing_v'] == 'OFF': - swing_L = 0b111 - elif args['swing_v'] == 'ON': - swing_L = 0b000 - elif (int(args['swing_v']) >= 0 and int(args['swing_v']) <= 5): - swing_L = int(args['swing_v']) - else: - raise ValueError(f"unrecognized swing vertical value {args['swing_v']}") # noqa E501 + if not isinstance(args['swing_h'], self.Swing_H): + raise ValueError("{} isn't a {} object".format( + args['swing_h'], self.Swing_H.__qualname__)) + swing_R = args['swing_h'].value + if not isinstance(args['swing_v'], self.Swing_V): + raise ValueError("{} isn't a {} object".format( + args['swing_v'], self.Swing_V.__qualname__)) + swing_L = args['swing_v'].value - if args['swing_h'] == 'OFF': - swing_R = 0b111 - elif args['swing_h'] == 'ON': - swing_R = 0b000 - else: - raise ValueError(f"unrecognized swing horizontal value {args['swing_h']}") # noqa E501 - - try: - mode_1 = { - 'auto': 0x00, - 'cooling': 0x20, - 'drying': 0x40, - 'heating': 0x80, - 'fan': 0xc0 - }[args['mode']] - except KeyError: - raise ValueError(f"unrecognized mode value {args['mode']}") + if not isinstance(args['mode'], self.Mode): + raise ValueError("{} isn't a {} object".format( + args['mode'], self.Mode.__qualname__)) + mode = args['mode'].value if args['mute'] and args['turbo']: raise ValueError("mute and turbo can't be on at once") @@ -508,46 +488,47 @@ def set_state(self, args: dict) -> bytes: speed_R = 0x80 if args['mode'] != 'fan': raise ValueError("mute is only available in fan mode") - args['speed'] = 'low' + args['speed'] = self.Speed.LOW elif args['turbo']: speed_R = 0x40 if args['mode'] not in ('cooling', 'heating'): raise ValueError("turbo is only available in cooling/heating") - args['speed'] = 'high' + args['speed'] = self.Speed.HIGH else: speed_R = 0x00 - try: - speed_L = { - 'high': 0x20, - 'mid': 0x40, - 'low': 0x60, - 'auto': 0xa0 - }[args['speed']] - except KeyError: - raise ValueError(f"unrecognized speed value: {args['speed']}") - - payload[0x0a] = (int(args['target_temp']) - 8 << 3) | swing_L - payload[0x0b] = (swing_R << 5) | cmnd_0b_rmask - payload[0x0c] = (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 - | cmnd_0c_rmask) - payload[0x0d] = speed_L - payload[0x0e] = speed_R - payload[0x0f] = mode_1 | (0b100 if args['sleep'] else 0b000) - # payload[0x10] = always 0x00 - # payload[0x11] = always 0x00 - payload[0x12] = (0b100000 if args['state'] else 0b000000 - | 0b100 if args['clean'] else 0b000 - | 0b11 if args['health'] else 0b00) - # payload[0x13] = always 0x00 - payload[0x14] = (0b10000 if args['display'] else 0b00000 - | 0b1000 if args['mildew'] else 0b0000) - # payload[0x15] = always 0x00 - payload[0x16] = cmnd_16 - - checksum = self._calculate_checksum(payload, 'little') - payload = self._encode(payload) + if not isinstance(args['speed'], self.Speed): + raise ValueError("{} isn't a {} object".format( + args['speed'], self.Speed.__qualname__)) + speed_L = args['speed'].value + payload = self._encode(bytes( + [ + 0x00, + 0x00, + 0x0f, + 0x00, + 0x01, + 0x01, + (int(args['target_temp']) - 8 << 3) | swing_L, + (swing_R << 5) | CMND_0B_RMASK, + (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 + | CMND_0C_RMASK), + speed_L, + speed_R, + mode | (0b100 if args['sleep'] else 0b000), + 0x00, + 0x00, + (0b100000 if args['state'] else 0b000000 + | 0b100 if args['clean'] else 0b000 + | 0b11 if args['health'] else 0b00), + 0x00, + (0b10000 if args['display'] else 0b00000 + | 0b1000 if args['mildew'] else 0b0000), + 0x00, + CMND_16 + ] + )) logging.debug("Constructed payload:\n%s", payload.hex(' ')) response = self.send_packet(0x6a, payload) @@ -558,14 +539,15 @@ def set_state(self, args: dict) -> bytes: # The first 12 bytes are always 0e 00 bb 00 07 00 00 00 04 00 01 01, # the next two should be the checksum of the sent command # and the last two are the checksum of the response. - if (response_payload[0xe:0x10] - == self._calculate_checksum(response_payload[2:0xe], 'little')): # noqa E501 - if response_payload[0xc:0xe] == checksum: + + if (response_payload[0xe:0x10] == self._calculate_checksum( + response_payload[2:0xe]).to_bytes(2, 'little')): + if response_payload[0xc:0xe] == payload[0x19:0x1b]: return True else: logging.warning( "Checksum in response %s different from sent payload %s", - response_payload[0xc:0xe].hex(), checksum.hex() + response_payload[0xc:0xe].hex(), payload[0x19:0x1b].hex() ) return False From 85ef56dd192ed38dea7f1526fec02cf73be25857 Mon Sep 17 00:00:00 2001 From: Enosh Date: Thu, 10 Dec 2020 17:09:51 +0200 Subject: [PATCH 25/26] Replace a bunch of type checks with creating a new instance. --- broadlink/climate.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 25f3331d..e0835250 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -468,19 +468,11 @@ def set_state(self, args: dict) -> bool: if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): raise ValueError(f"target_temp out of range, value: {args['target_temp']}") # noqa E501 - if not isinstance(args['swing_h'], self.Swing_H): - raise ValueError("{} isn't a {} object".format( - args['swing_h'], self.Swing_H.__qualname__)) - swing_R = args['swing_h'].value - if not isinstance(args['swing_v'], self.Swing_V): - raise ValueError("{} isn't a {} object".format( - args['swing_v'], self.Swing_V.__qualname__)) - swing_L = args['swing_v'].value - - if not isinstance(args['mode'], self.Mode): - raise ValueError("{} isn't a {} object".format( - args['mode'], self.Mode.__qualname__)) - mode = args['mode'].value + # Creating a new instance verifies the type + swing_R = self.Swing_H(args['swing_h']).value + swing_L = self.Swing_V(args['swing_v']).value + + mode = self.Mode(args['mode']).value if args['mute'] and args['turbo']: raise ValueError("mute and turbo can't be on at once") @@ -497,10 +489,7 @@ def set_state(self, args: dict) -> bool: else: speed_R = 0x00 - if not isinstance(args['speed'], self.Speed): - raise ValueError("{} isn't a {} object".format( - args['speed'], self.Speed.__qualname__)) - speed_L = args['speed'].value + speed_L = self.Speed(args['speed']).value payload = self._encode(bytes( [ @@ -549,5 +538,9 @@ def set_state(self, args: dict) -> bool: "Checksum in response %s different from sent payload %s", response_payload[0xc:0xe].hex(), payload[0x19:0x1b].hex() ) + else: + logging.warning( + "Unable to verify request because response appears to be bad." + ) return False From bb1371971bb2456591918722c239908e8ca3e7e0 Mon Sep 17 00:00:00 2001 From: Enosh Date: Fri, 11 Dec 2020 19:37:56 +0200 Subject: [PATCH 26/26] Move packet creation to a new _send_command and to _enocde. --- broadlink/climate.py | 220 ++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 127 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index e0835250..5a57ba0d 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -2,6 +2,7 @@ from typing import List import logging from enum import IntEnum, unique +import struct from .device import device from .exceptions import check_error @@ -245,9 +246,6 @@ def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: class sq1(device): """Controls Tornado SMART X SQ series air conditioners.""" - REQUEST_PREFIX = bytes([0xbb, 0x00, 0x06, 0x80]) - RESPONSE_PREFIX = bytes([0xbb, 0x00, 0x07, 0x00]) - @unique class Mode(IntEnum): AUTO = 0 @@ -264,18 +262,18 @@ class Speed(IntEnum): AUTO = 0xa0 @unique - class Swing_H(IntEnum): + class SwingH(IntEnum): ON = 0b000, OFF = 0b111 @unique - class Swing_V(IntEnum): + class SwingV(IntEnum): ON = 0b000, - POS_1 = 1 - POS_2 = 2 - POS_3 = 3 - POS_4 = 4 - POS_5 = 5 + POS1 = 1 + POS2 = 2 + POS3 = 3 + POS4 = 4 + POS5 = 5 OFF = 0b111 def __init__(self, *args, **kwargs): @@ -283,7 +281,16 @@ def __init__(self, *args, **kwargs): self.type = "Tornado SQ air conditioner" def _decode(self, response) -> bytes: + # RESPONSE_PREFIX = bytes([0xbb, 0x00, 0x07, 0x00, 0x00, 0x00]) payload = self.decrypt(bytes(response[0x38:])) + + length = int.from_bytes(payload[:2], 'little') + checksum = self._calculate_checksum( + payload[2:length]).to_bytes(2, 'little') + if checksum == payload[length:length+2]: + logging.debug("Checksum incorrect (calculated %s actual %s).", + checksum.hex(), payload[length:length+2].hex()) + return payload def _calculate_checksum(self, payload: bytes) -> int: @@ -301,59 +308,61 @@ def _calculate_checksum(self, payload: bytes) -> int: s = (s & 0xffff) + (s >> 16) return (0xffff - s) - def _encode(self, payload: bytes) -> bytes: - """Encode payload (add length to beginning, checksum after payload).""" - payload = self.REQUEST_PREFIX + payload + def _encode(self, data: bytes) -> bytes: + """Encode data for transport.""" + payload = struct.pack("HHHH", 0x00BB, 0x8006, 0x0000, len(data)) + data + logging.debug("Payload:\n%s", payload.hex(' ')) checksum = self._calculate_checksum(payload).to_bytes(2, 'little') return (len(payload) + 2).to_bytes(2, 'little') + payload + checksum - def _send_short_payload(self, payload: int) -> bytes: - """Send a request for info from AC unit and returns the response. - 0 = GET_AC_INFO, 1 = GET_STATES, 2 = GET_SLEEP_INFO - """ - packet = bytes( - [0x00, 0x00, 0x02, 0x00] - + { - 0: [0x21, 0x01], - 1: [0x11, 0x01], - 2: [0x41, 0x01] - }[payload] - ) + def _send_command(self, command: int, data: bytes = b'') -> bytes: + """Send a command to the unit. - response = self.send_packet(0x6a, self._encode(packet)) + Known commands: + - Get AC info: 0x0121 + - Get states: 0x0111 + - Get sleep info: 0x0141 + """ + packet = self._encode(command.to_bytes(2, "little") + data) + logging.debug("Payload:\n%s", packet.hex(' ')) + response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - return (self._decode(response)) + return self._decode(response) def get_state(self) -> dict: """Returns a dictionary with the unit's parameters. Returns: dict: - state (bool): power + power (bool): target_temp (float): temperature set point 16> 3) + (0.0 if (payload[0xe] & 0b10000000) == 0 else 0.5)) # noqa E501 - data['swing_h'] = self.Swing_H((payload[0x0d] & 0b11100000) >> 5) - data['swing_v'] = self.Swing_V(payload[0x0c] & 0b111) + data['swing_h'] = self.SwingH((payload[0x0d] & 0b11100000) >> 5) + data['swing_v'] = self.SwingV(payload[0x0c] & 0b111) data['mode'] = self.Mode(payload[0x11] & ~ 0b111) @@ -370,15 +379,6 @@ def get_state(self) -> dict: data['display'] = bool(payload[0x16] & 0b10000) data['mildew'] = bool(payload[0x16] & 0b1000) - checksum = self._calculate_checksum(payload[2:0x19] - ).to_bytes(2, 'little') - if payload[0x19:0x1b] != checksum: - logging.warning("checksum fail: calculated %s actual %s", - checksum.hex(), payload[0x19:0x1b].hex()) - - logging.debug("Received payload:\n%s", payload.hex(' ')) - logging.debug("0b[R] mask: %x, 0c[R] mask: %x, cmnd_16: %x", - payload[0x0d] & 0xf, payload[0x0e] & 0xf, payload[0x18]) logging.debug("Data: %s", data) return data @@ -391,10 +391,12 @@ def get_ac_info(self) -> dict: state (bool): power ambient_temp (float): ambient temperature """ - payload = self._send_short_payload(0) + payload = self._send_command(0x121) if (len(payload) != 48): raise ValueError(f"unexpected payload size: {len(payload)}") + logging.debug("Received payload:\n%s", payload.hex(' ')) + # Length is 34 (0x22), the next 11 bytes are # the same: bb 00 07 00 00 00 18 00 01 21 c0, # bytes 0x23,0x24 are the checksum. @@ -406,33 +408,27 @@ def get_ac_info(self) -> dict: data['ambient_temp'] = (ambient_temp + float(payload[0x21] & 0b00011111) / 10.0) - checksum = self._calculate_checksum(payload[2:0x23]).to_bytes(2, 'big') - if (payload[0x23:0x25] != checksum): - logging.warning("checksum fail: calculated %s actual %s", - checksum.hex(), payload[0x23:0x25].hex()) - - logging.debug("Received payload:\n%s", payload.hex(' ')) - + logging.debug("Data: %s", data) return data - def set_state(self, args: dict) -> bool: + def set_state(self, state: dict) -> bool: """Set parameters of unit. Args: - args (dict): if any are missing the current value will be retrived - state (bool): power + state (dict): if any are missing the current value will be retrived + power (bool): target_temp (float): temperature set point 16 bool: CMND_0C_RMASK = 0b1101 CMND_16 = 0b101 - keys = ['state', 'mode', 'target_temp', 'speed', 'mute', 'turbo', + keys = ['power', 'mode', 'target_temp', 'speed', 'mute', 'turbo', 'swing_v', 'swing_h', 'sleep', 'display', 'health', 'clean', 'mildew'] - unknown_keys = [key for key in args.keys() if key not in keys] + unknown_keys = [key for key in state.keys() if key not in keys] if len(unknown_keys) > 0: raise ValueError(f"unknown argument(s) {unknown_keys}") - missing_keys = [key for key in keys if key not in args] + missing_keys = [key for key in keys if key not in state] if len(missing_keys) > 0: try: received_state = self.get_state() @@ -459,88 +455,58 @@ def set_state(self, args: dict) -> bool: received_state = self.get_state() else: raise e - logging.debug("Raw args %s", args) - received_state.update(args) - args = received_state - logging.debug("Filled args %s", args) + logging.debug("Raw state %s", state) + received_state.update(state) + state = received_state + logging.debug("Filled state %s", state) - args['target_temp'] = round(args['target_temp'] * 2) / 2 - if not (args['target_temp'] >= 16 and args['target_temp'] <= 32): - raise ValueError(f"target_temp out of range, value: {args['target_temp']}") # noqa E501 + state['target_temp'] = round(state['target_temp'] * 2) / 2 + if not (16 <= state['target_temp'] <= 32): + raise ValueError(f"target_temp out of range: {state['target_temp']}") # noqa E501 # Creating a new instance verifies the type - swing_R = self.Swing_H(args['swing_h']).value - swing_L = self.Swing_V(args['swing_v']).value + swing_R = self.SwingH(state['swing_h']) + swing_L = self.SwingV(state['swing_v']) - mode = self.Mode(args['mode']).value + mode = self.Mode(state['mode']) - if args['mute'] and args['turbo']: + if state['mute'] and state['turbo']: raise ValueError("mute and turbo can't be on at once") - elif args['mute']: + elif state['mute']: speed_R = 0x80 - if args['mode'] != 'fan': + if state['mode'] != 'fan': raise ValueError("mute is only available in fan mode") - args['speed'] = self.Speed.LOW - elif args['turbo']: + state['speed'] = self.Speed.LOW + elif state['turbo']: speed_R = 0x40 - if args['mode'] not in ('cooling', 'heating'): + if state['mode'] not in ('cooling', 'heating'): raise ValueError("turbo is only available in cooling/heating") - args['speed'] = self.Speed.HIGH + state['speed'] = self.Speed.HIGH else: speed_R = 0x00 - speed_L = self.Speed(args['speed']).value + speed_L = self.Speed(state['speed']) - payload = self._encode(bytes( + data = bytes( [ - 0x00, - 0x00, - 0x0f, - 0x00, - 0x01, - 0x01, - (int(args['target_temp']) - 8 << 3) | swing_L, + (int(state['target_temp']) - 8 << 3) | swing_L, (swing_R << 5) | CMND_0B_RMASK, - (0b10000000 if (args['target_temp'] % 1 == 0.5) else 0 - | CMND_0C_RMASK), + ((state['target_temp'] % 1 == 0.5) << 7) | CMND_0C_RMASK, speed_L, speed_R, - mode | (0b100 if args['sleep'] else 0b000), + mode | (state['sleep'] << 2), 0x00, 0x00, - (0b100000 if args['state'] else 0b000000 - | 0b100 if args['clean'] else 0b000 - | 0b11 if args['health'] else 0b00), + (state['power'] << 5 | state['clean'] << 2 | 0b11 if state['health'] else 0b00), 0x00, - (0b10000 if args['display'] else 0b00000 - | 0b1000 if args['mildew'] else 0b0000), + state['display'] << 4 | state['mildew'] << 3, 0x00, CMND_16 ] - )) - logging.debug("Constructed payload:\n%s", payload.hex(' ')) + ) + logging.debug("Constructed payload data:\n%s", data.hex(' ')) - response = self.send_packet(0x6a, payload) - check_error(response[0x22:0x24]) - response_payload = self._decode(response) + response_payload = self._send_command(0x0101, data) logging.debug("Response payload:\n%s", response_payload.hex(' ')) - # Response payloads are 16 bytes long. - # The first 12 bytes are always 0e 00 bb 00 07 00 00 00 04 00 01 01, - # the next two should be the checksum of the sent command - # and the last two are the checksum of the response. - - if (response_payload[0xe:0x10] == self._calculate_checksum( - response_payload[2:0xe]).to_bytes(2, 'little')): - if response_payload[0xc:0xe] == payload[0x19:0x1b]: - return True - else: - logging.warning( - "Checksum in response %s different from sent payload %s", - response_payload[0xc:0xe].hex(), payload[0x19:0x1b].hex() - ) - else: - logging.warning( - "Unable to verify request because response appears to be bad." - ) - - return False + # Response payloads are 16 bytes long, + # Bytes 0d-0e are the checksum of the sent command.