From c47e42c775ecc41b6daa94948046652f6f4e30b5 Mon Sep 17 00:00:00 2001 From: scott francis Date: Fri, 10 Jul 2020 11:06:47 -0700 Subject: [PATCH] first version --- PentairProtocol.py | 305 +++++++++++++++++++++++++++++++++++++++++++++ PentairSerial.py | 50 ++++++++ README.md | 129 ++++++++++++++++++- pentair-control.py | 152 ++++++++++++++++++++++ 4 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 PentairProtocol.py create mode 100644 PentairSerial.py create mode 100755 pentair-control.py diff --git a/PentairProtocol.py b/PentairProtocol.py new file mode 100644 index 0000000..c9adca2 --- /dev/null +++ b/PentairProtocol.py @@ -0,0 +1,305 @@ +from collections.abc import Iterable +from functools import reduce +import json +import re +import struct + + +RECORD_SEPARATOR = b'\xFF\x00\xFF' + + +class Payload: + def __init__(self, body): + self.status = {} + self.body = body + + # self.dumpBody() + + def dump(self): + if len(self.status) > 0: + print(json.dumps(self.status)) + else: + self.dumpBody() + + def dumpBody(self): + print(' '.join(f'{b:02X}' for b in self.body)) + + + def getStatus(self): + return self.status + +# +# lots of payload cracking things taken from +# https://docs.google.com/document/d/1M0KMfXfvbszKeqzu6MUF_7yM6KDHk8cZ5nrH1_OUcAc/edit +# + +class DatePayload(Payload): + def __init__(self, body): + super().__init__(body) + + try: + self.status['hour'] = self.body[0] + self.status['min'] = self.body[1] + self.status['dow'] = self.body[2] + self.status['day'] = self.body[3] + self.status['month'] = self.body[4] + self.status['year'] = self.body[5] + self.status['adjust'] = self.body[6] + self.status['dst'] = self.body[7] + except Exception as err: + pass + + +class StatusPayload(Payload): + # circuit bit-masks for byte 2 + SPA = 0x01 + AUX1 = 0x02 + AUX2 = 0x04 + AUX3 = 0x08 + POOL = 0x20 + FEATURE1 = 0x10 + FEATURE2 = 0x40 + FEATURE3 = 0x80 + # byte 3 + FEATURE4 = 0x01 + + # modes for byte 9 + RUN_MODE = 0x01 # Run Mode (Normal/Service) + TEMP_UNITS = 0x04 # Temp Unit (F/C), + FREEZE_PROTECT = 0x08 # Freeze Protection (Off/On) + TIMEOUT = 0x10 # Timeout (Off/On) + + # byte 10 + HEATER_ON = 0x0F + # HEATER_OFF = 0x03 # seems to be wrong... off is off 0x00 + + # byte 12 + DELAY = 0x04 + + # byte 22 -- masks -- pool is low nibble, spa is high nibble (realy low 2 bits of high nibble) + HEATER_POOL_OFF = 0x00 + HEATER_POOL_EN = 0x01 + HEATER_POOL_SOLAR_PREF = 0x02 + HEATER_POOL_SOLAR_EN = 0x03 + + HEATER_SPA_OFF = 0x00 + HEATER_SPA_EN = 0x10 + + def __init__(self, body): + super().__init__(body) + + try: + self.status['hour'] = self.body[0] + self.status['min'] = self.body[1] + + self.status['spa'] = (self.body[2] & self.SPA) != 0 + self.status['aux1'] = (self.body[2] & self.AUX1) != 0 + self.status['aux2'] = (self.body[2] & self.AUX2) != 0 + self.status['aux3'] = (self.body[2] & self.AUX3) != 0 + self.status['pool'] = (self.body[2] & self.POOL) != 0 + self.status['feature1'] = (self.body[2] & self.FEATURE1) != 0 + self.status['feature2'] = (self.body[2] & self.FEATURE2) != 0 + self.status['feature3'] = (self.body[2] & self.FEATURE3) != 0 + + self.status['feature4'] = (self.body[3] & self.FEATURE4) != 0 + + # byte 4 - 8 are 0 + if reduce((lambda x, sum: sum + x), self.body[4:8]) != 0: + print('unusual bytes 4 - 8 in StatusPayload') + + self.status['runMode'] = self.body[9] & self.RUN_MODE + self.status['tempUnits'] = self.body[9] & self.TEMP_UNITS + self.status['freezeProtect'] = self.body[9] & self.FREEZE_PROTECT + self.status['timeout'] = self.body[9] & self.TIMEOUT + + # if body[10] != self.HEATER_OFF and body[10] != self.HEATER_ON: + # print(f'unusual heater setting in StatusPayload {body[10]:02X}') + self.status['heater'] = (self.body[10] == self.HEATER_ON) + + if self.body[11] != 0: + print('unusual byte 11 in StatusPayload') + + # if (body[12] & 0x30) != 0x30: + # print(f'unusual byte 12 in StatusPayload {body[12]:02X}') + self.status['delay'] = self.body[12] & self.DELAY + + self.status['waterTemp'] = self.body[14] # repeated in body[15] + self.status['waterTemp2'] = self.body[15] + self.status['airTemp'] = self.body[18] + self.status['solarTemp'] = self.body[19] + + self.status['poolHeaterMode'] = self.body[22] & 0x0F + self.status['spaHeaterMode'] = self.body[22] & 0xF0 + + except Exception as err: + pass + + +class PumpPayload(Payload): + # sample: 0A 02 02 03 03 09 60 00 00 00 00 00 01 00 0F + def __init__(self, body): + super().__init__(body) + try: + self.status['pumpStarted'] = (self.body[0] & 0x0A) != 0 + + (self.status['pumpMode'], + self.status['pumpState'], + self.status['pumpWatts'], + self.status['pumpRPM'] ) = struct.unpack(">BBHH", self.body[1:9]) + + print(f"read RPM {self.status['pumpRPM']} from:"); self.dumpBody() + + # there are a lot more bytes... seem to be sequence number... + except Exception as err: + pass + + +class PingPayload(Payload): + # think this is just a ping to see if the other party is alive + def __init__(self, body): + super().__init__(body) + try: + if self.body[0] != 0xFF: + print(f'unkown ping data: {body[0]:02X}') + + except Exception as err: + pass + + def dump(self): + pass + +class PumpStatus(Payload): + PUMP_STARTED = 0x0A + PUMP_STOPPED = 0x04 + + def __init__(self, body): + super().__init__(body) + + try: + self.status['pumpStarted'] = (self.body[0] & 0x0A) != 0 + except Exception as err: + pass + +class CommandPayload(Payload): + def __init__(self, body): + super().__init__(body) + print("Command Payload") + self.dumpBody() + + try: + (self.status['pumpRPM'],) = struct.unpack(">H", self.body[-2:]) + print(f"read RPM {self.status['pumpRPM']} from:"); self.dumpBody() + + except Exception as err: + pass + +# 24,0F,10,08,0D,4C 4C 3D 55 64 00 00 00 00 00 00 00 00 +# temperatures: water water air water-set spa-set +class TempPayload(Payload): + def __init__(self, body): + super().__init__(body) + + try: + # 8 unused bytes... probably solar and other features I don't have + (self.status['waterTemp'], + self.status['waterTemp2'], + self.status['airTemp'], + self.status['poolSetTemp'], + self.status['spaSetTemp']) = struct.unpack("bbbbbxxxxxxxx", self.body) + + except Exception as err: + pass + + +class PentairProtocol: + IDLE_BYTE = b'\xFF' + START_BYTE = 0xA5 + + def __init__(self): + self.payloads = { + 0x00: { 0x01: CommandPayload, + 0x04: PingPayload, + 0x06: PumpStatus, + 0x07: PumpPayload }, + 0x24: { 0x02: StatusPayload, + 0x05: DatePayload, + 0x08: TempPayload } + } + + self.state = {} + + # + # validFrame + # + # returns validity of the unpadded frame (that is, remove padding before calling) + # + # A frame has: + # START_BYTE 1 bytes -- but this is stripped... + # version 1 bytes + # data 0+ bytes + # checksum 2 bytes Big Endian + # + # frame is validated to have this structure and checksum calculated on frame [:-2] + # that is... the full frame less the check sum. The checksum is the sum of START, version, + # and all databytes modulo 2^16 + # + def validFrame(self, f): + try: + valid = f[0] == self.START_BYTE and ((f[-2] << 8) + f[-1]) & 0xFFFF == reduce((lambda x, sum: sum + x), f[:-2]) + # valid = ((f[-2] << 8) + f[-1]) & 0xFFFF == reduce((lambda x, sum: sum + x), f[:-2], 0xA5) + + except Exception as e: + valid = False + + return valid + + # + # parseFrame + # + # returns a dict with the parsed components of the frame + # DOES NOT PARSE PAYLOAD or SPECIFIC MESSAGE CODES + # + def parseFrame(self, f): + if not self.validFrame(f): + return {} + + parsed = {} + try: + parsed['type'] = f[1] + parsed['destination'] = f[2] + parsed['source'] = f[3] + parsed['command'] = f[4] + + parsed['payloadLength'] = f[5] + parsed['payload'] = f[6:-2] + + except Exception as e: + pass + + return parsed + + + def parseEvents(self, events): + while events: + e = events[0].rstrip(self.IDLE_BYTE) + # print(e) + + if self.validFrame(e): + frame = self.parseFrame(e) + + try: + if frame['payloadLength'] != len(frame['payload']): + raise Exception + + payload = self.payloads[frame['type']][frame['command']](frame['payload']) + self.state.update(payload.getStatus()) # just overwrite ... and get the latest + # payload.dump() + + except Exception as err: + # print(err) + print(",".join(list(map((lambda x: f'{x:02X}' if not isinstance(x, Iterable) else ' '.join(f'{b:02X}' for b in x) if len(x) > 0 else ''), list(frame.values()))))) + pass + + events = events[1:] + + return self.state diff --git a/PentairSerial.py b/PentairSerial.py new file mode 100644 index 0000000..b94276a --- /dev/null +++ b/PentairSerial.py @@ -0,0 +1,50 @@ +# See https://web.archive.org/web/20171130091506/https:/www.sdyoung.com/home/decoding-the-pentair-easytouch-rs-485-protocol/ +# For details on pentair protocol +# +import serial + + +class PentairSerial: + def __init__(self, device, separator): + self.device = device + self.separator = separator + # configure the serial connections (the parameters differs on the device you are connecting to) + self.ser = serial.Serial( + port=self.device, + baudrate=9600, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, timeout=0.1, write_timeout=0.1 + ) + + self.readbuffer = '' + + def open(self): + self.ser.open() + + def isOpen(self): + return self.ser.isOpen() + + def send(self, commands): + self.ser.write((self.RECORD_SEPARATOR.join( + commands) + self.RECORD_SEPARATOR).encode()) + + def listen(self): + events = [] + n = self.ser.inWaiting() + if n > 0: + try: + self.readbuffer = self.ser.read(n) #.decode('ascii') + e = self.readbuffer.split(self.separator) + + # if self.readbuffer[-1] == self.RECORD_SEPARATOR: + # events = e + # self.readbuffer = '' + # else: + # emit the records that are complete + events = e[:-1] + self.readbuffer = e[-1] # retain the partial ones + except Exception as e: + print("Exception " + e + " while reading from Serial port") + + return events diff --git a/README.md b/README.md index 25aa12c..e951d91 100644 --- a/README.md +++ b/README.md @@ -1 +1,128 @@ -# Pentair-Thing \ No newline at end of file +# Pentair-Thing + +A rudimentary AWS IoT Thing implementation for a Pentair EasyTouch controller. + +While this project targets AWS IoT / Greengrass integration, the components should be modular enough to help anyone else integrate with other automation systems and to serve as a platform for decoding Pentair protocol for additional equipment. + +* Current Status: Framework for capturing messages on the 485 bus and decoding payloads works. Payload decoders built for most common messages I see on my system +Useful for understanding and decoding live protocol or collecting traffic samples. + +_TODO_ +1. Integrate latest AWS IoT Device SDK to publish messages to Greengrass (this will also facilitate debug) +2. Migrate protocol decoding to Greengrass Lambdas +3. Build coherent model for 'the pool' from collected messages and maintain model in Device Shadow +4. Implement commands: SPA On, Pool On, All Off, Heat Pool, Heat Spa, Heat Off + +### Purpose + +To facilitate monitoring and control of residential pool equipment using the Pentair EasyTouch controller + +### Background / Context + +I have an EasyTouch controller, a Variable Speed Pump, a Booster pump for the sweep, a Jet pump for the spa, two controlled valves (to switch between pool and spa), and a gas Heater. The controller is mounted outside near the equipment, but I mostly interract with the equipment using the EasyTouch wireless remote. Equipment was installed around 2010, so there may be more modern versions. I don't have Solar heating, water features, or poly-chromic lighting--so the project won't have info about those systems, but should help someone discover the protocol for those. + +The EasyTouch remote UX is extremely clunky. To do simple things are many menus deep with cryptic select/menu key sequences. My family has had a very hard time working with this system. This project supports an overall goal to make monitoring and control of the equipment more friendly and predictable by extending status and control through AWS IoT Core, a mobile app, and an Alexa skill to enable control of the system by phone, web, or voice. + +### Integration Points + +The EasyTouch wireless remote interfaces to the controller with a wireless transceiver. The transceiver connects to the controller over a 4 wire EIA-485 interface. This interface (485) also seems to be used for the pump and other machines, although EIA-232 direct links also seem to be used. Pentair does not publish any API, SDK, HDK, or other developer information that is available to customers. In fact, I couldn't even find any pay-for or partner resources either. + +Some people have been successful in creating interfaces for their own purposes and a few have been gracious enough to post this information publicly. Here are some links to the resources I've found helpful in building this project: + +* [Pentair EasyTouch Installation Guide](https://www.pentair.com/content/dam/extranet/aquatics/residential-pool/owners-manuals/automation/easytouch/easytouch-control-system-pl4-psl4-installation-guide.pdf) +* [SD Young's work to decode the protocol](https://web.archive.org/web/20171130091506/https:/www.sdyoung.com/home/decoding-the-pentair-easytouch-rs-485-protocol/) +* [Josh Block's notes on decoding](https://docs.google.com/document/d/1M0KMfXfvbszKeqzu6MUF_7yM6KDHk8cZ5nrH1_OUcAc/edit) + +## Hardware Interface + +For this project, I have connected a Raspberry Pi Zero Wireless to the EasyTouch Controller over a 4-wire EIA-485 link. The controller seems to source enough power at 15V DC to power the Pi using a [buck converter](https://docs.google.com/document/d/1M0KMfXfvbszKeqzu6MUF_7yM6KDHk8cZ5nrH1_OUcAc/edit). The 'A' and 'B' signals are decoded with an [EIA-485 Transceiver Module](https://wiki.seeedstudio.com/Grove-RS485/) and routed to UART0 on the Pi. All the wiring is done on a [proto board](https://www.adafruit.com/product/571?gclid=Cj0KCQjwo6D4BRDgARIsAA6uN18kixfuLTQC-J3qJR--7Gl3GOsgvus298pjwGht9j0Ftrn9X5r5PccaApFrEALw_wcB) and a pi-standard 40-pin ribbon cable. All of this is mounted in an [IP66 Case](https://www.amazon.com/gp/product/B077QM9VM9/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1) and connected to the controller over standard sprinkler wire. + +My house uses an [eero mesh wifi network](https://www.amazon.com/gp/product/B07WMLPSRL/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1) which provides good coverage for Pi, mounted near the controller. + +_NOTE_: It is necessary to enable the Serial port on the Pi and for the serial port to NOT be login shell. This is accomplished with the `raspi-config` tool. No other special configuration of the Pi was needed beyond current (2020-05-29) version of Pi OS (or Raspbian or whatever they call it this week). + +## the code + +`pentair-control.py` is the main entry point. It sets up user options, the Serial interface, and the protocol decoder. It is intended to be run from a command similar to +``` +pentair-control.py -p /dev/ttyS0 -t 60 +``` + +The serial port is read by `PentairSerial.py` and splits the read buffer into 'events' or 'records' using a configurable record separator. + +`PentairProtocol.py` defines this separator and decodes the framing and payload. + +### Decoding the Protocol + +EIA-485 is designed as a multi-drop loop with no dedicated clock line. This means that devices must agree on datarate (baud rate). When no message is being broadcast (and since it's a common pair of wires, it's all broadcast), bytes are read as `0xFF` by the serial port. This means that there is *ALWAYS* something to read from the serial port. + +In 485 protocols, there is typically a 'start' signal to indicate something useful is coming. In the Pentair implementation, this indicated by two consecutive bytes of `0x00` and then `0xFF`. Thus the 'record separator' is `0xFF 0x00 0xFF` where there could be a long string of `0xFF` preceding the separator. When these records are split, there will likely be many 'idle bytes' of `0xFF` at the end of each record. `PentairProtocol` strips these off the end. + +Events are interpreted as frames inside `PentairProtocol` according to a definition that has been emperically determined by the above resources, with a bit of my own sleuthing. The raw serial would look something like: +``` +FFFFFFFFFF00FFA5001060010960FFFFFF +``` +One way to loook at the raw payload is to use picocom and `xxd`. +``` +mkfifo apipe +picocom -b 9600 /dev/ttyS0 -l | tee apipe +``` +in a second terminal: +``` +xxd apipe +``` +Note the long sequence of IDLE BYTES before and after this short message. If the bytes come up inverted from this, you likely have the 'A' and 'B' connections reveresed. Either reverse them, or modify the code to invert all the bits. + +Frames seem to be + +| Field | example bytes | definition | +| ------- | ---------- | ---------- | +| IDLE BYTES+ | FFFF... | any number | +| START | 00FF | this ends the record separator | +| FRAME_START | A5 | included in checksum for frame. bit pattern ensure proper data rate | +| TYPE | [00,24] | some people also see 01. could indicate versions or other protocols | +| DST | [0F, 10, 60,...] | address of intended recipient | +| SRC | [10, 60, ...] | address of sender | +| CMD | [01, 03, 08,...] | could also be considered as message id | +| LEN | 0D | length of payload | +| PAYLOAD | XX | LEN number of bytes | +| CHECKSUM | XXXX | two byte modulo sum of all bytes from FRAME_START (inclusive) through all of PAYLOAD | +| IDLE_BYTES+ | FFFF... | not really part of the frame, but FFs will follow any framing | + +Inside `PentairProtocol`, frames are validated (by checksup) and if succeeded, the payload is loaded. It is important to validate the Checksum first as this is shared bus and any device can assert at anytime -- causing collisions and garbled data. + +PAYLOADs are interpreted based on the TYPE and CMD values. Additionally, it may be valuable to track SRCs and DSTs or only use one side. + +Some common addresses for DSTs and SRCs: +| ADDR | Device | +| ---- | ------ | +| 0F | broadcast? that is... everyone should pay attention? | +| 10 | controller -- could probably MOSTLY just use message FROM this addr | +| 60 | pump | + +Some common TYPEs and CMDs +| TYPE | CMD | meaning | +| --- | ---- | ----- | +| 00 | 01 | command, or circuit change, or other modification -- sets pump speed and maybe valves or other? | +| 00 | 04 | seems to be some kind of heartbeat or ping from the controller to a device (usually the pump - 60) and then a matching response | +| 00 | 06 | pump status -- is it running/started (0A) or stopped (04) | +| 00 | 07 | pump data -- started, mode, watts, rpm, etc. | +| 24 | 02 | status -- the motherload of info... time, date, temps, lots of stuff | +| 24 | 05 | date -- current controller date, happens every 2s or so | +| 24 | 08 | temps -- air, water, preferred, solar, other temperatures | + +Seems like device 10 is the only one sending TYPE 24s. These may be the main informational messages. The TYPE 00 messages seem to occur in pairs SRC -> DST then a complementary 'ACK' messages from DST -> SRC. + +### Interpreting Payloads +A Simple, polymorphic class structure is implemented where specific payloads are intpreted based on TYPE and CMD. To add new interpreters: +1. Sub-Class Payload +2. Call `super().__init__(body)` in `__init__()` +3. Use `struct` or other means to interpret the payload and set `self.status` + +The `status` member dicts will be aggregated (updated) -- creating a simple 'state' that I intend to use for shadow updates. + +### Debugging the Protocol + +It can be handy to print various things as you debug the messages. There are some handy utilities to dump the payload as either the interpreted structure or the raw frame (formatted as hex). The exception block in `parseEvents()` is particularly useful as this will catch the 'unhandled' payloads. + +It can also be handy to dump every frame, modifying the format to be CSV, and redirecting that data to a file for analysis with Excel or whatever. \ No newline at end of file diff --git a/pentair-control.py b/pentair-control.py new file mode 100755 index 0000000..15458fb --- /dev/null +++ b/pentair-control.py @@ -0,0 +1,152 @@ +#!/usr/bin/python3 + +# from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient +import PentairProtocol +import PentairSerial +# import Shadow + +import argparse +from datetime import datetime +import json +import logging +import time + +# remote debug harness -- unco +# import ptvsd +# import socket +# this_ip = (([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")] or [[(s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) + ["no IP found"])[0] +# ptvsd.enable_attach(address=(this_ip,3000), redirect_output=True) +# ptvsd.wait_for_attach() +# end debug harness + + +# Read in command-line parameters +parser = argparse.ArgumentParser() +# IOT args +# parser.add_argument("-e", "--endpoint", action="store", required=True, dest="host", help="Your AWS IoT custom endpoint") +# parser.add_argument("-r", "--rootCA", action="store", required=True, dest="rootCAPath", help="Root CA file path") +# parser.add_argument("-c", "--cert", action="store", dest="certificatePath", help="Certificate file path") +# parser.add_argument("-k", "--key", action="store", dest="privateKeyPath", help="Private key file path") +# parser.add_argument("-w", "--websocket", action="store_true", dest="useWebsocket", default=False, +# help="Use MQTT over WebSocket") +# parser.add_argument("-n", "--thingName", action="store", dest="thingName", default="Bot", help="Targeted thing name") + +# serial port args +parser.add_argument("-p", "--port", action="store", required=True, dest="port", default="/dev/ttyS0", help="Serial Port Device") +parser.add_argument("-t", "--timeout", action="store", required=True, dest="timeout", default="0.5", help="Timeout to wait for events") +# parser.add_argument("-q", "--query", action="store", dest="query", default="['Mute','Power','Video','Volume']", help="Inital queries to kick things off") + + +args = parser.parse_args() +# host = args.host +# rootCAPath = args.rootCAPath +# certificatePath = args.certificatePath +# privateKeyPath = args.privateKeyPath +# useWebsocket = args.useWebsocket +# thingName = args.thingName +# clientId = args.thingName + +port = args.port +timeout = float(args.timeout) +# query = eval(args.query) + + +# if args.useWebsocket and args.certificatePath and args.privateKeyPath: +# parser.error("X.509 cert authentication and WebSocket are mutual exclusive. Please pick one.") +# exit(2) + +# if not args.useWebsocket and (not args.certificatePath or not args.privateKeyPath): +# parser.error("Missing credentials for authentication.") +# exit(2) + +# Configure logging +logger = logging.getLogger("Pentair-Thing.core") +logger.setLevel(logging.INFO) +streamHandler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +streamHandler.setFormatter(formatter) +logger.addHandler(streamHandler) + +# setup serial & protocol +connection = PentairSerial.PentairSerial(port, PentairProtocol.RECORD_SEPARATOR) +protocol = PentairProtocol.PentairProtocol() + +def customShadowCallback_Update(payload, responseStatus, token): + # payload is a JSON string ready to be parsed using json.loads(...) + # in both Py2.x and Py3.x + if responseStatus == "timeout": + logger.debug("Update request " + token + " time out!") + # if responseStatus == "accepted": + # payloadDict = json.loads(payload) + # print("~~~~~~~~~~~~~~~~~~~~~~~") + # print("Update request with token: " + token + " accepted!") + # print("payload: " + json.dumps(payloadDict)) #["state"]["desired"]["property"])) + # print("~~~~~~~~~~~~~~~~~~~~~~~\n\n") + if responseStatus == "rejected": + logger.debug("Update request " + token + " rejected!") + +def customShadowCallback_Delta(payload, responseStatus, token): + logger.debug("Received a delta message:") + payloadDict = json.loads(payload) + deltaMessage = json.dumps(payloadDict["state"]) + logger.debug(deltaMessage + "\n") + + commands = protocol.makeCommands(payloadDict["state"]) + logger.debug("\nbuilt commands: " + str(commands) + "\n") + connection.send(commands) + + +# Init AWSIoTMQTTShadowClient +# myAWSIoTMQTTShadowClient = None +# if useWebsocket: +# myAWSIoTMQTTShadowClient = AWSIoTMQTTShadowClient(clientId, useWebsocket=True) +# myAWSIoTMQTTShadowClient.configureEndpoint(host, 443) +# myAWSIoTMQTTShadowClient.configureCredentials(rootCAPath) +# else: +# myAWSIoTMQTTShadowClient = AWSIoTMQTTShadowClient(clientId) +# myAWSIoTMQTTShadowClient.configureEndpoint(host, 8883) +# myAWSIoTMQTTShadowClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath) + +# AWSIoTMQTTShadowClient configuration +# myAWSIoTMQTTShadowClient.configureAutoReconnectBackoffTime(1, 32, 20) +# myAWSIoTMQTTShadowClient.configureConnectDisconnectTimeout(10) # 10 sec +# myAWSIoTMQTTShadowClient.configureMQTTOperationTimeout(5) # 5 sec + +# Connect to AWS IoT +# myAWSIoTMQTTShadowClient.connect() + +# Create a deviceShadow with persistent subscription +# deviceShadowHandler = myAWSIoTMQTTShadowClient.createShadowHandlerWithName(thingName, True) + +# Listen on deltas +# deviceShadowHandler.shadowRegisterDeltaCallback(customShadowCallback_Delta) + +state = {} +def do_something(): + if not connection.isOpen(): + connection.open() + + # listen for status events + events = connection.listen() + state.update(protocol.parseEvents(events)) + logger.info( str(datetime.now()) + " - " + json.dumps(state) + "\n") + + # try: + # deviceShadowHandler.shadowUpdate(Shadow.makeStatePayload("reported", state), customShadowCallback_Update, 5) + # except Exception as e: + # print(e) + + + +def run(): + if not connection.isOpen(): + connection.open() + + while True: + time.sleep(0.9*timeout) # crude approach to timing adjustment + do_something() + +if __name__ == "__main__": + + # do_something(connection, protocol) + run()