From 475b6dc8611a7bc3199cd334bf98a094ea005b30 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Mon, 24 Sep 2018 21:22:49 +1000 Subject: [PATCH 01/11] Dynalite objects and event firing --- .gitignore | 3 + dynalite/Dynalite.py | 362 ------------------------------------ dynalite/__init__.py | 1 - dynalite_lib/__init__.py | 2 + dynalite_lib/__version__.py | 4 + dynalite_lib/dynalite.py | 206 ++++++++++++++++++++ setup.py | 2 +- test.py | 57 +++--- 8 files changed, 243 insertions(+), 394 deletions(-) delete mode 100644 dynalite/Dynalite.py delete mode 100644 dynalite/__init__.py create mode 100644 dynalite_lib/__init__.py create mode 100644 dynalite_lib/__version__.py create mode 100644 dynalite_lib/dynalite.py diff --git a/.gitignore b/.gitignore index 0d6cd81..ba53970 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ venv.bak/ .mypy_cache/ .dmypy.json dmypy.json + +# Testing Data +test/ diff --git a/dynalite/Dynalite.py b/dynalite/Dynalite.py deleted file mode 100644 index 6000ab8..0000000 --- a/dynalite/Dynalite.py +++ /dev/null @@ -1,362 +0,0 @@ -import socket -import time -import threading - - -class AreaPreset: - area = None - preset = None - - def __init__(self, area, preset, dynet): - self.area = area - self.preset = preset - self._state = False - - def turn_on(self): - dynet.setPreset(self.area, self.preset) - self._state = True - return True - - def turn_off(self): - self._state = False - return True - - def ison(self): - return self._state - - def update(self): - dynet.reqPreset(self.area) - - def setState(self, state): - self._state = state - - def __repr__(self): - return str(self.__dict__) - - -class DynaliteEvent: - type = 'unknown' - area = None - preset = None - status = None - host = None - port = None - fade = None - dim = None - msg = None - object = None - - def __init__(self, msg=None): - self.msg = msg - - def __repr__(self): - return str(self.__dict__) - - -class Dynalite: - def __init__(self, host, port=12345): - ''' Constructor for this class. ''' - self.host = host - self.port = port - # How many bytes in a message (not including checksum) - self.messagesize = 7 - self.template = bytearray([28, 0, 0, 0, 0, 0, 0]) - self.connected = False - self.timeout = 900 - self.areaPresets = {} - - def connect(self, handler=None): - self.postHandler = handler - thread = threading.Thread(target=self.socketReceive, args=()) - thread.daemon = False # No not daemonize thread - thread.start() # Start the execution - - def handler(self, event): - now = time.strftime("%c") - if event.type == 'preset': - if event.area is not None and event.preset is not None: - self.setAreaPreset(event.area, event.preset, True) - elif event.type == 'presetupdate': - if event.area is not None and event.preset is not None: - self.setAreaPreset(event.area, event.preset, True) - if self.postHandler is not None: - return self.postHandler(event) - else: - print("%s %s" % (now, event)) - return True - - def connectSocket(self): - self.connected = False - - while not self.connected: - try: - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.connect((self.host, self.port)) - self.s.settimeout(self.timeout) - self.connected = True - event = DynaliteEvent() - event.type = 'connection' - event.status = 'connected' - event.host = self.host - event.port = self.port - self.handler(event) - except (socket.error, socket.timeout) as e: - event = DynaliteEvent() - event.type = 'connection' - event.status = 'failed' - event.host = self.host - event.port = self.port - self.handler(event) - time.sleep(2) - return True - - def socketReceive(self): - if self.connected is not True: - self.connectSocket() - while True: - try: - buf = self.s.recv(1024) - self.handler(self.process_message(buf)) - except (socket.error, socket.timeout) as e: - self.connectSocket() - - def getAreaPresets(self, area): - if area not in self.areaPresets: - self.areaPresets[area] = {} - return self.areaPresets[area] - - def setAreaPreset(self, area, preset, state=None): - if area not in self.areaPresets: - self.areaPresets[area] = {} - event = DynaliteEvent() - event.type = 'newarea' - event.area = area - self.handler(event) - if preset not in self.areaPresets[area]: - self.areaPresets[area][preset] = AreaPreset(area, preset, self) - event = DynaliteEvent() - event.type = 'newpreset' - event.area = area - event.preset = preset - event.object = self.areaPresets[area][preset] - self.handler(event) - if state is not None: - self.areaPresets[area][preset].setState(state) - if state is True: - self.areaPresets[area]['_current'] = preset - for p in self.areaPresets[area]: - if p != preset and p != '_current': - self.areaPresets[area][p].setState(state) - - def getHost(self): - return self.host - - def getPort(self): - return self.port - - def getMessageSize(self): - return self.messagesize - - def getTemplate(self): - return self.template - - def process_message(self, msg): - if self.valid_checksum(msg) is not True: - return False - sync = msg[0] - area = msg[1] - data1 = msg[2] - opcode = msg[3] - data2 = msg[4] - data3 = msg[5] - join = msg[6] - chk = msg[7] - - if sync != 28: - event = DynaliteEvent(data) - event.type = 'in' - event.error = 'Not a logical message' - return self.handler(event) - - if opcode < 9: - event = self.process_preset( - area, opcode, data1, data2, data3, join) - event.msg = msg - elif opcode == 72: - event = self.process_indicatorled(area, data1, data2, data3, join) - event.msg = msg - elif opcode == 98: - event = self.process_areastatus(area, data1) - event.msg = msg - elif opcode == 99: - event = self.process_reqareastatus(area) - event.msg = msg - elif opcode == 101: - event = self.process_linearpreset(area, data1, data2, data3, join) - event.msg = msg - else: - event = DynaliteEvent(msg) - event.type = 'unknown' - event.area = area - - if len(msg) > self.messagesize + 1: - print("Extra %s bytes:\t%s" % - (len(msg) - self.messagesize + 1, msg[self.messagesize + 1:])) - self.process_message(msg[self.messagesize + 1:]) - - return event - - def valid_checksum(self, msg): - if len(msg) < self.messagesize + 1: - return False - tocheck = bytearray( - [msg[0], msg[1], msg[2], msg[3], msg[4], msg[5], msg[6]]) - chk = int(self.calc_checksum(tocheck), 16) - if msg[7] != chk: - return False - return True - - def calc_checksum(self, s): - """ - Calculates checksum for sending commands to the ELKM1. - Sums the ASCII character values mod256 and returns - the lower byte of the two's complement of that value. - """ - return '%2X' % (-(sum(ord(c) for c in "".join(map(chr, s))) % 256) & 0xFF) - - def send(self, data): - if self.connected is not True: - self.connectSocket() - - data.append(int(self.calc_checksum(data), 16)) - - try: - self.s.sendall(data) - event = DynaliteEvent(data) - event.type = 'out' - self.handler(event) - self.process_message(data) - time.sleep(0.2) - except (socket.error, socket.timeout) as e: - self.connectSocket() - - def process_preset(self, area, opcode, fadeLow, fadeHigh, bank, join): - preset = (opcode + (bank * 8)) + 1 - fade = (fadeLow + (fadeHigh * 256)) * 0.02 - event = DynaliteEvent() - event.type = 'preset' - event.area = area - event.preset = preset - event.fade = fade - return event - - def process_linearpreset(self, area, preset, fadeLow, fadeHigh, join): - preset = preset + 1 - fade = (fadeLow + (fadeHigh * 256)) * 0.02 - event = DynaliteEvent() - event.type = 'preset' - event.area = area - event.preset = preset - event.fade = fade - return event - - def process_indicatorled(self, area, type, dimming, fadeVal, join): - if type == 1: - typeName = 'indicator' - elif type == 2: - typeName = 'backlight' - else: - typeName = 'unknown' - - if fadeVal > 0: - fade = fadeVal / 50 - else: - fade = 0 - - dimpc = round((256 - dimming) / 255, 2) * 100 - - event = DynaliteEvent() - event.type = 'indicator_' + typeName - event.area = area - event.dim = dimpc - event.fade = fade - return event - - def process_areastatus(self, area, preset): - preset = preset + 1 - event = DynaliteEvent() - event.type = 'presetupdate' - event.area = area - event.preset = preset - return event - - def process_reqareastatus(self, area): - event = DynaliteEvent() - event.type = 'presetrequest' - event.area = area - return event - - def reqPreset(self, area): - cmd = self.template[:] - cmd[1] = area - cmd[3] = 99 - cmd[6] = 255 - self.send(cmd) - return True - - def setPreset(self, area, preset, fade=2, join=255): - cmd = self.template[:] - cmd[1] = area - if fade == 0: - cmd[2] = 0 - cmd[4] = 0 - else: - cmd[2] = int(fade / 0.02) - (int((fade / 0.02) / 256) * 256) - cmd[4] = int((fade / 0.02) / 256) - - cmd[6] = join - - if preset < 1: - return False - elif preset > 64: - return False - - bank = int((preset - 1) / 8) - opcode = int(preset - (bank * 8)) - 1 - - if opcode > 3: - opcode = opcode + 6 - - cmd[3] = opcode - cmd[5] = bank - - self.send(cmd) - return True - - def setIndicatorLED(self, area, type, dimpc, fadeVal=2, join=255): - cmd = self.template[:] - cmd[1] = area - cmd[2] = type - cmd[3] = 72 - - if dimpc == 0: - cmd[4] = 255 - elif dimpc == 1: - cmd[4] = 255 - elif dimpc < 0: - cmd[4] = 255 - elif dimpc > 100: - cmd[4] = 1 - else: - cmd[4] = int(256 - (dimpc / 100 * 255)) - - if fadeVal < 0: - cmd[5] = 0 - elif fadeVal > 5: - cmd[5] = 255 - else: - cmd[5] = int(fadeVal * 50) - - cmd[6] = join - self.send(cmd) - return True diff --git a/dynalite/__init__.py b/dynalite/__init__.py deleted file mode 100644 index 6559fc7..0000000 --- a/dynalite/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from Dynalite import Dynalite diff --git a/dynalite_lib/__init__.py b/dynalite_lib/__init__.py new file mode 100644 index 0000000..fba3d51 --- /dev/null +++ b/dynalite_lib/__init__.py @@ -0,0 +1,2 @@ +"""Dynalite Communications""" +from .dynalite import Dynalite diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py new file mode 100644 index 0000000..cee5474 --- /dev/null +++ b/dynalite_lib/__version__.py @@ -0,0 +1,4 @@ +"""Version of package.""" +VERSION = (0, 1, 1) + +__version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py new file mode 100644 index 0000000..5bb7b53 --- /dev/null +++ b/dynalite_lib/dynalite.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +@ Author : Troy Kelly +@ Date : 23 Sept 2018 +@ Description : Philips Dynalite Library - Unofficial interface for Philips Dynalite over RS485 + +@ Notes: Requires a RS485 to IP gateway (Do not use the Dynalite one - use something cheaper) +""" + +import asyncio +import logging +import json + + +class BroadcasterError(Exception): + def __init__(self, message): + self.message = message + + +class PresetError(Exception): + def __init__(self, message): + self.message = message + + +class AreaError(Exception): + def __init__(self, message): + self.message = message + + +class Event(object): + + def __init__(self, eventType=None, message=None, data={}): + self.eventType = eventType.upper() if eventType else None + self.msg = message + self.data = data + + def toJson(self): + return json.dumps(self.__dict__) + + +class DynaliteConfig(object): + + def __init__(self, config=None): + self.log_level = config['log_level'].upper( + ) if 'log_level' in config else logging.INFO + self.log_formatter = config[ + 'log_formatter'] if 'log_formatter' in config else "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" + self.host = config['host'] if 'host' in config else 'localhost' + self.port = config['port'] if 'port' in config else 12345 + self.default = config['default'] if 'default' in config else {} + self.area = {} + self.preset = {} + self.area = config['area'] if 'area' in config else {} + self.preset = config['preset'] if 'preset' in config else {} + + +class Broadcaster(object): + + def __init__(self, listenerFunction=None, loop=None): + if listenerFunction is None: + raise BroadcasterError( + "A broadcaster bust have a listener Function") + self._listenerFunction = listenerFunction + self._monitoredEvents = [] + self._loop = loop + + def monitorEvent(self, eventType=None): + if eventType is None: + raise BroadcasterError("Must supply an event type to monitor") + if eventType not in self._monitoredEvents: + self._monitoredEvents.append(eventType.upper()) + + def unmonitorEvent(self, eventType=None): + if eventType is None: + raise BroadcasterError("Must supply an event type to un-monitor") + if eventType in self._monitoredEvents: + self._monitoredEvents.remove(eventType.upper()) + + def update(self, event=None): + if event is None: + return + if event.eventType not in self._monitoredEvents and '*' not in self._monitoredEvents: + return + if self._loop: + self._loop.create_task(self._callUpdater(event=event)) + else: + self._listenerFunction(event) + + @asyncio.coroutine + def _callUpdater(self, event=None): + self._listenerFunction(event) + + +class DynalitePreset(object): + + def __init__(self, name=None, value=None, fade=2, logger=None, broadcastFunction=None): + if not value: + raise PresetError("A preset must have a value") + self._logger = logger + self.active = False + self.name = name if name else "Preset " + str(value) + self.value = value + self.fade = fade + + +class DynaliteArea(object): + + def __init__(self, name=None, value=None, fade=2, areaPresets=None, defaultPresets=None, logger=None, broadcastFunction=None): + if not value: + raise PresetError("An area must have a value") + self._logger = logger + self.name = name if name else "Area " + str(value) + self.value = value + self.fade = fade + self.preset = {} + if areaPresets: + for presetValue in areaPresets: + preset = areaPresets[presetValue] + presetName = preset['name'] if 'name' in preset else None + presetFade = preset['fade'] if 'fade' in preset else fade + self._logger.debug("Area '%s' - Creating '%d/%s' (Fade %d)" % + (self.name, int(presetValue), presetName, presetFade)) + self.preset[int(presetValue)] = DynalitePreset( + name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=broadcastFunction) + if broadcastFunction: + broadcastData = { + 'area': self.value, + 'preset': presetValue, + 'name': self.name + ' ' + presetName, + 'state': 'OFF' + } + broadcastFunction( + Event(eventType='newpreset', data=broadcastData)) + if defaultPresets: + for presetValue in defaultPresets: + if int(presetValue) not in self.preset: + preset = defaultPresets[presetValue] + presetName = preset['name'] if preset['name'] else None + presetFade = preset['fade'] if preset['fade'] else fade + self._logger.debug("Area '%s' - Creating '%d/%s' (Fade %d)" % + (self.name, int(presetValue), presetName, presetFade)) + self.preset[int(presetValue)] = DynalitePreset( + name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=broadcastFunction) + if broadcastFunction: + broadcastData = { + 'area': self.value, + 'preset': presetValue, + 'name': self.name + ' ' + presetName, + 'state': 'OFF' + } + broadcastFunction( + Event(eventType='newpreset', data=broadcastData)) + + +class Dynalite(object): + + def __init__(self, config=None, loop=None, logger=None): + self.loop = loop if loop else asyncio.get_event_loop() + self._logger = logger if logger else logging.getLogger(__name__) + self._config = DynaliteConfig(config=config) + logging.basicConfig(level=self._config.log_level, + format=self._config.log_formatter) + + self._listeners = [] + + self._devices = { + 'area': {} + } + + def start(self): + self.loop.create_task(self._start()) + + def broadcast(self, event): + self.loop.create_task(self._broadcast(event)) + + @asyncio.coroutine + def _broadcast(self, event): + for listenerFunction in self._listeners: + listenerFunction.update(event) + + @asyncio.coroutine + def _start(self): + for areaValue in self._config.area: + areaName = self._config.area[areaValue]['name'] if 'name' in self._config.area[areaValue] else None + areaPresets = self._config.area[areaValue]['preset'] if 'preset' in self._config.area[areaValue] else { + } + areaFade = self._config.area[areaValue]['fade'] if 'fade' in self._config.area[areaValue] else None + if areaFade is None: + areaFade = self._config.default['fade'] if 'fade' in self._config.default else 2 + if 'nodefault' in self._config.area[areaValue] and self._config.area[areaValue]['nodefault'] == True: + defaultPresets = None + else: + defaultPresets = self._config.preset + + self._logger.debug( + "Generating Area '%d/%s' with a default fade of %d" % (int(areaValue), areaName, areaFade)) + self._devices['area'][int(areaValue)] = DynaliteArea( + name=areaName, value=areaValue, fade=areaFade, areaPresets=areaPresets, defaultPresets=defaultPresets, logger=self._logger, broadcastFunction=self.broadcast) + + def addListener(self, listenerFunction=None): + broadcaster = Broadcaster( + listenerFunction=listenerFunction, loop=self.loop) + self._listeners.append(broadcaster) + return broadcaster diff --git a/setup.py b/setup.py index 33fe301..cfe9c08 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.0.6", + version="0.1.1", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", diff --git a/test.py b/test.py index f24c8bf..3b8abb4 100755 --- a/test.py +++ b/test.py @@ -1,34 +1,31 @@ #!/usr/bin/env python3 -import time -# Import classes -from Dynalite import Dynalite +import json +import logging +import asyncio +from dynalite_lib import Dynalite + +logging.basicConfig(level=logging.DEBUG, + format="[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s") +LOG = logging.getLogger(__name__) + +OPTIONS_FILE = 'test/options.json' + +loop = asyncio.get_event_loop() -HOST = '10.7.3.212' # Standard loopback interface address (localhost) -PORT = 12345 # Port to listen on (non-privileged ports are > 1023) def handleEvent(event): - print(event) - return True - -# Create an object -dynet = Dynalite.Dynalite(HOST, PORT) - -dynet.connect() -time.sleep(0.5) -dynet.setPreset(8,9,2) -time.sleep(2) -dynet.setPreset(8,4,2) -time.sleep(2) -dynet.reqPreset(1) -time.sleep(2) -dynet.reqPreset(2) -time.sleep(2) -dynet.reqPreset(3) -time.sleep(2) -dynet.reqPreset(4) -time.sleep(2) -dynet.reqPreset(5) -time.sleep(2) -dynet.reqPreset(6) -time.sleep -print(dynet.areaPresets) + LOG.info("Received Event: %s" % event.eventType) + LOG.debug(event.toJson()) + + +if __name__ == '__main__': + with open(OPTIONS_FILE, 'r') as f: + cfg = json.load(f) + + dynalite = Dynalite(config=cfg, loop=loop) + + bcstr = dynalite.addListener(listenerFunction=handleEvent) + bcstr.monitorEvent('*') + + dynalite.start() + loop.run_forever() From ee4f174de40ceed5065864c219bb4b55dfefa86a Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Thu, 27 Sep 2018 17:18:14 +1000 Subject: [PATCH 02/11] Basic preset updating and control --- dynalite_lib/dynalite.py | 163 +++++++++++++++------ dynalite_lib/dynet.py | 309 +++++++++++++++++++++++++++++++++++++++ test.py | 13 +- 3 files changed, 442 insertions(+), 43 deletions(-) create mode 100644 dynalite_lib/dynet.py diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index 5bb7b53..d72dc1d 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - """ @ Author : Troy Kelly @ Date : 23 Sept 2018 @@ -12,6 +9,7 @@ import asyncio import logging import json +from .dynet import Dynet, DynetControl class BroadcasterError(Exception): @@ -69,89 +67,123 @@ def __init__(self, listenerFunction=None, loop=None): def monitorEvent(self, eventType=None): if eventType is None: raise BroadcasterError("Must supply an event type to monitor") + eventType = eventType.upper() if eventType not in self._monitoredEvents: self._monitoredEvents.append(eventType.upper()) def unmonitorEvent(self, eventType=None): if eventType is None: raise BroadcasterError("Must supply an event type to un-monitor") + eventType = eventType.upper() if eventType in self._monitoredEvents: self._monitoredEvents.remove(eventType.upper()) - def update(self, event=None): + def update(self, event=None, dynalite=None): if event is None: return if event.eventType not in self._monitoredEvents and '*' not in self._monitoredEvents: return if self._loop: - self._loop.create_task(self._callUpdater(event=event)) + self._loop.create_task(self._callUpdater( + event=event, dynalite=dynalite)) else: - self._listenerFunction(event) + self._listenerFunction(event=event, dynalite=dynalite) @asyncio.coroutine - def _callUpdater(self, event=None): - self._listenerFunction(event) + def _callUpdater(self, event=None, dynalite=None): + self._listenerFunction(event=event, dynalite=dynalite) class DynalitePreset(object): - def __init__(self, name=None, value=None, fade=2, logger=None, broadcastFunction=None): + def __init__(self, name=None, value=None, fade=2, logger=None, broadcastFunction=None, area=None, dynetControl=None): if not value: raise PresetError("A preset must have a value") self._logger = logger self.active = False self.name = name if name else "Preset " + str(value) - self.value = value - self.fade = fade + self.value = int(value) + self.fade = float(fade) + self.area = area + self.broadcastFunction = broadcastFunction + self._control = dynetControl + if self.broadcastFunction: + broadcastData = { + 'area': self.area.value, + 'preset': self.value, + 'name': self.area.name + ' ' + self.name, + 'state': 'OFF' + } + self.broadcastFunction( + Event(eventType='newpreset', data=broadcastData)) + + def turnOn(self, send=True): + self.active = True + if self.broadcastFunction: + broadcastData = { + 'area': self.area.value, + 'preset': self.value, + 'name': self.area.name + ' ' + self.name, + 'state': 'ON' + } + self.broadcastFunction( + Event(eventType='preset', data=broadcastData)) + if send and self._control: + self._control.areaPreset( + area=self.area.value, preset=self.value, fade=self.fade) + for preset in self.area.preset: + if self.value != preset: + if self.area.preset[preset].active: + self.area.preset[preset].turnOff(send=False) + + def turnOff(self, send=True): + self.active = False + if self.broadcastFunction: + broadcastData = { + 'area': self.area.value, + 'preset': self.value, + 'name': self.area.name + ' ' + self.name, + 'state': 'OFF' + } + self.broadcastFunction( + Event(eventType='preset', data=broadcastData)) + if send and self._control: + self._control.areaOff(area=self.area.value, fade=self.fade) class DynaliteArea(object): - def __init__(self, name=None, value=None, fade=2, areaPresets=None, defaultPresets=None, logger=None, broadcastFunction=None): + def __init__(self, name=None, value=None, fade=2, areaPresets=None, defaultPresets=None, logger=None, broadcastFunction=None, dynetControl=None): if not value: raise PresetError("An area must have a value") self._logger = logger self.name = name if name else "Area " + str(value) - self.value = value + self.value = int(value) self.fade = fade self.preset = {} + self.broadcastFunction = broadcastFunction + self._dynetControl = dynetControl if areaPresets: for presetValue in areaPresets: preset = areaPresets[presetValue] presetName = preset['name'] if 'name' in preset else None presetFade = preset['fade'] if 'fade' in preset else fade - self._logger.debug("Area '%s' - Creating '%d/%s' (Fade %d)" % - (self.name, int(presetValue), presetName, presetFade)) self.preset[int(presetValue)] = DynalitePreset( - name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=broadcastFunction) - if broadcastFunction: - broadcastData = { - 'area': self.value, - 'preset': presetValue, - 'name': self.name + ' ' + presetName, - 'state': 'OFF' - } - broadcastFunction( - Event(eventType='newpreset', data=broadcastData)) + name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) if defaultPresets: for presetValue in defaultPresets: if int(presetValue) not in self.preset: preset = defaultPresets[presetValue] presetName = preset['name'] if preset['name'] else None presetFade = preset['fade'] if preset['fade'] else fade - self._logger.debug("Area '%s' - Creating '%d/%s' (Fade %d)" % - (self.name, int(presetValue), presetName, presetFade)) self.preset[int(presetValue)] = DynalitePreset( - name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=broadcastFunction) - if broadcastFunction: - broadcastData = { - 'area': self.value, - 'preset': presetValue, - 'name': self.name + ' ' + presetName, - 'state': 'OFF' - } - broadcastFunction( - Event(eventType='newpreset', data=broadcastData)) + name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) + + def presetOn(self, preset, send=True): + if preset not in self.preset: + self.preset[preset] = DynalitePreset( + value=preset, fade=self.fade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) + self.preset[preset].turnOn(send=send) class Dynalite(object): @@ -163,25 +195,73 @@ def __init__(self, config=None, loop=None, logger=None): logging.basicConfig(level=self._config.log_level, format=self._config.log_formatter) + self._configured = False + self._listeners = [] - self._devices = { + self.devices = { 'area': {} } + self._dynet = None + self.control = None + def start(self): self.loop.create_task(self._start()) + def connect(self): + self.loop.create_task(self._connect()) + + def processTraffic(self, event): + self.loop.create_task(self._processTraffic(event)) + + @asyncio.coroutine + def _processTraffic(self, event): + if event.eventType == 'AREAPRESET': + self.devices['area'][event.data['area'] + ].presetOn(event.data['preset'],send=False) + else: + broadcastData = { + 'area': event.data['area'], + 'namename': self.devices['area'][event.data['area']].name, + 'data': event.data.toJson() + } + self.broadcast( + Event(eventType='unknown', data=broadcastData)) + + @asyncio.coroutine + def _connect(self): + self._dynet = Dynet(host=self._config.host, port=self._config.port, + loop=self.loop, broadcaster=self.processTraffic, onConnect=self._connected, onDisconnect=self._disconnection) + self._dynet.connect() + + @asyncio.coroutine + def _connected(self, dynet=None, transport=None): + self.control = DynetControl( + dynet, self.loop, areaDefinition=self.devices['area']) + if not self._configured: + self.loop.create_task(self._configure()) + self.broadcast(Event(eventType='connected', data={})) + + @asyncio.coroutine + def _disconnection(self, dynet=None): + self.control = None + self.broadcast(Event(eventType='disconnected', data={})) + def broadcast(self, event): self.loop.create_task(self._broadcast(event)) @asyncio.coroutine def _broadcast(self, event): for listenerFunction in self._listeners: - listenerFunction.update(event) + listenerFunction.update(event=event, dynalite=self) @asyncio.coroutine def _start(self): + self.connect() + + @asyncio.coroutine + def _configure(self): for areaValue in self._config.area: areaName = self._config.area[areaValue]['name'] if 'name' in self._config.area[areaValue] else None areaPresets = self._config.area[areaValue]['preset'] if 'preset' in self._config.area[areaValue] else { @@ -196,8 +276,9 @@ def _start(self): self._logger.debug( "Generating Area '%d/%s' with a default fade of %d" % (int(areaValue), areaName, areaFade)) - self._devices['area'][int(areaValue)] = DynaliteArea( - name=areaName, value=areaValue, fade=areaFade, areaPresets=areaPresets, defaultPresets=defaultPresets, logger=self._logger, broadcastFunction=self.broadcast) + self.devices['area'][int(areaValue)] = DynaliteArea( + name=areaName, value=areaValue, fade=areaFade, areaPresets=areaPresets, defaultPresets=defaultPresets, logger=self._logger, broadcastFunction=self.broadcast, dynetControl=self.control) + self._configured = True def addListener(self, listenerFunction=None): broadcaster = Broadcaster( diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py new file mode 100644 index 0000000..743104d --- /dev/null +++ b/dynalite_lib/dynet.py @@ -0,0 +1,309 @@ +""" +@ Author : Troy Kelly +@ Date : 23 Sept 2018 +@ Description : Philips Dynalite Library - Unofficial interface for Philips Dynalite over RS485 + +@ Notes: Requires a RS485 to IP gateway (Do not use the Dynalite one - use something cheaper) +""" + +import asyncio +import logging +import json + +LOG = logging.getLogger(__name__) + + +class DynetError(Exception): + def __init__(self, message): + self.message = message + + +class PacketError(Exception): + def __init__(self, message): + self.message = message + + +class DynetPacket(object): + + def __init__(self, msg=None): + self.sync = None + self.area = None + self.data = [] + self.command = None + self.join = None + self.chk = None + if msg is not None: + self.fromMsg(msg) + + def toMsg(self, sync=28, area=0, command=0, data=[0, 0, 0], join=255): + bytes = [] + bytes.append(sync) + bytes.append(area) + bytes.append(data[0]) + bytes.append(command) + bytes.append(data[1]) + bytes.append(data[2]) + bytes.append(join) + bytes.append(self.calcsum(bytes)) + self.fromMsg(bytes) + + def fromMsg(self, msg): + bytes = [] + for byte in msg: + bytes.append(int(byte)) + self._msg = bytes + if(len(self._msg) > 8): + self.excess = self._msg[7:] + self._msg = self._msg[:7] + if self.calcsum(self._msg) != self._msg[7]: + raise PacketError("Failed checksum %s" % self._msg) + self.sync = self._msg[0] + self.area = self._msg[1] + self.data = [self._msg[2], self._msg[4], self._msg[5]] + self.command = self._msg[3] + self.join = self._msg[6] + self.chk = self._msg[7] + if self.sync == 28: + if self.command < 4 or (self.command > 9 and self.command < 14): + if self.command > 3: + self.preset = self.command - 6 + else: + self.preset = self.command + self.preset = (self.preset + (self.data[2] * 8)) + 1 + self.fade = (self.data[0] + (self.data[1] * 256)) * 0.02 + if self.command == 101: + self.preset = self.data[0] + 1 + self.fade = (self.data[1] + (self.data[2] * 256)) * 0.02 + + def toJson(self): + return json.dumps(self.__dict__) + + def calcsum(self, msg): + msg = msg[:7] + return (-(sum(ord(c) for c in "".join(map(chr, msg))) % 256) & 0xFF) + + +class DynetEvent(object): + + def __init__(self, eventType=None, message=None, data={}, direction=None): + self.eventType = eventType.upper() if eventType else None + self.msg = message + self.data = data + self.direction = direction + + def toJson(self): + return json.dumps(self.__dict__) + + +class DynetConnection(asyncio.Protocol): + + def __init__(self, connectionMade=None, connectionLost=None, receiveHandler=None, connectionPause=None, connectionResume=None, loop=None): + self._transport = None + self._paused = False + self._loop = loop + self.connectionMade = connectionMade + self.connectionLost = connectionLost + self.receiveHandler = receiveHandler + self.connectionPause = connectionPause + self.connectionResume = connectionResume + + def connection_made(self, transport): + self._transport = transport + self._paused = False + if self.connectionMade is not None: + if self._loop is None: + self.connectionMade(transport) + else: + self._loop.create_task(self.connectionMade(transport)) + + def connection_lost(self, exc=None): + self._transport = None + if self.connectionLost is not None: + if self._loop is None: + self.connectionLost(exc) + else: + self._loop.create_task(self.connectionLost(exc)) + + def pause_writing(self): + self._paused = True + if self.connectionPause is not None: + if self._loop is None: + self.connectionPause() + else: + self._loop.create_task(self.connectionPause()) + + def resume_writing(self): + self._paused = False + if self.connectionResume is not None: + if self._loop is None: + self.connectionResume() + else: + self._loop.create_task(self.connectionResume()) + + def data_received(self, data): + LOG.debug("Data Received: %s" % data) + if self.receiveHandler is not None: + if self._loop is None: + self.receiveHandler(data) + else: + self._loop.create_task(self.receiveHandler(data)) + + def eof_received(self): + LOG.debug("EOF Received") + + +class DynetControl(object): + + def __init__(self, dynet, loop, areaDefinition=None): + self._dynet = dynet + self._loop = loop + self._area = areaDefinition + + def areaPreset(self, area, preset, fade=2): + self._loop.create_task(self._areaPreset(area=area,preset=preset,fade=fade)) + + @asyncio.coroutine + def _areaPreset(self, area, preset, fade): + packet = DynetPacket() + preset = preset - 1 + bank = int((preset)/8) + opcode = preset-(bank*8) + if opcode > 3: + opcode = opcode + 6 + fadeLow = int(fade / 0.02) - (int((fade / 0.02) / 256) * 256) + fadeHigh = int((fade / 0.02) / 256) + packet.toMsg(sync=28, area=area, command=opcode, data=[fadeLow, fadeHigh, bank], join=255) + self._dynet.write(packet) + + def areaOff(self, area, fade=2): + self._loop.create_task(self._areaOff(area=area,fade=fade)) + + @asyncio.coroutine + def _areaOff(self, area, fade): + packet = DynetPacket() + if fade > 25.5: + fade = 25.5 + if fade < 0: + fade = 0 + packet.toMsg(sync=28, area=area, command=104, data=[255, 0, int(fade*10)], join=255) + self._dynet.write(packet) + + + +class Dynet(object): + + def __init__(self, host=None, port=None, broadcaster=None, onConnect=None, onDisconnect=None, loop=None): + if host is None or port is None or loop is None: + raise DynetError( + 'Must supply a host, port and loop for Dynet connection') + self._host = host + self._port = port + self._loop = loop + self.broadcast = broadcaster + self._onConnect = onConnect + self._onDisconnect = onDisconnect + self._conn = lambda: DynetConnection(connectionMade=self._connection, connectionLost=self._disconnection, + receiveHandler=self._receive, connectionPause=self._pause, connectionResume=self._resume, loop=self._loop) + self._transport = None + self._handlers = {} + self._connection_retry_timer = 1 + self._paused = False + self._inBuffer = [] + self._outBuffer = [] + self._timeout = 30 + + def cleanup(self): + self._connection_retry_timer = 1 + self._inBuffer = [] + self._outBuffer = [] + self._transport = None + + def connect(self, onConnect=None): + asyncio.ensure_future(self._connect()) + + async def _connect(self): + LOG.debug("Connecting to Dynet on %s:%d" % (self._host, self._port)) + try: + await asyncio.wait_for(self._loop.create_connection(self._conn, host=self._host, port=self._port), timeout=self._timeout) + except (ValueError, OSError, asyncio.TimeoutError) as err: + LOG.warning("Could not connect to Dynet (%s). Retrying in %d seconds", + err, self._connection_retry_timer) + self._loop.call_later(self._connection_retry_timer, self.connect) + self._connection_retry_timer = 2 * \ + self._connection_retry_timer if self._connection_retry_timer < 32 else 60 + + @asyncio.coroutine + def _receive(self, data): + packet = DynetPacket(data) + if hasattr(packet, 'preset'): + event = DynetEvent(eventType='AREAPRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area':packet.area,'preset':packet.preset,'fade':packet.fade,'join':packet.join}, direction="IN") + self.broadcast(event) + else: + LOG.debug("Dynet Inbound: %s" % packet.toJson()) + + @asyncio.coroutine + def _pause(self): + LOG.debug("Pausing Dynet on %s:%d" % (self._host, self._port)) + # Need to schedule a resend here + self._paused = True + + @asyncio.coroutine + def _resume(self): + LOG.debug("Resuming Dynet on %s:%d" % (self._host, self._port)) + # Need to schedule a resend here + self._paused = False + + @asyncio.coroutine + def _connection(self, transport=None): + LOG.debug("Connected to Dynet on %s:%d" % (self._host, self._port)) + self.cleanup() + if not transport is None: + self._transport = transport + if self._onConnect is not None: + self._loop.create_task(self._onConnect(dynet=self,transport=transport)) + else: + raise DynetError("Connected but not transport channel provided") + + @asyncio.coroutine + def _disconnection(self, exc=None): + LOG.debug("Disconnected from Dynet on %s:%d" % + (self._host, self._port)) + self.cleanup() + if self._onDisconnect is not None: + self._loop.create_task(self._onDisconnect(dynet=self)) + + if exc is not None: + LOG.error(exc) + + def write(self, packet=None): + self._loop.create_task(self._write(packet)) + + @asyncio.coroutine + def _write(self, newPacket=None): + if self._transport is None: + raise DynetError("Must be connected to write/send messages") + + if newPacket is not None: + self._outBuffer.append(newPacket) + + if self._paused: + LOG.info("Connection paused - queuing packet") + self._loop.call_later(1, self.updateLocations) + + for idx, packet in enumerate(self._outBuffer): + msg = bytearray() + msg.append(packet.sync) + msg.append(packet.area) + msg.append(packet.data[0]) + msg.append(packet.command) + msg.append(packet.data[1]) + msg.append(packet.data[2]) + msg.append(packet.join) + msg.append(packet.chk) + try: + self._transport.write(msg) + LOG.debug("Dynet Sent: %s" % msg) + except: + self._logger.error("Unable to write data: %s" % msg) + del self._outBuffer[idx] + #self._loop.create_task(self._receive(msg)) diff --git a/test.py b/test.py index 3b8abb4..855cbea 100755 --- a/test.py +++ b/test.py @@ -11,13 +11,19 @@ OPTIONS_FILE = 'test/options.json' loop = asyncio.get_event_loop() +dynalite = None -def handleEvent(event): - LOG.info("Received Event: %s" % event.eventType) +def handleEvent(event=None, dynalite=None): + #LOG.info("Received Event: %s" % event.eventType) LOG.debug(event.toJson()) +def handleConnect(event=None, dynalite=None): + LOG.warning("Connected to Dynalite") + dynalite.devices['area'][2].preset[1].turnOn() + + if __name__ == '__main__': with open(OPTIONS_FILE, 'r') as f: cfg = json.load(f) @@ -27,5 +33,8 @@ def handleEvent(event): bcstr = dynalite.addListener(listenerFunction=handleEvent) bcstr.monitorEvent('*') + onConnect = dynalite.addListener(listenerFunction=handleConnect) + onConnect.monitorEvent('CONNECTED') + dynalite.start() loop.run_forever() From 98cec50a2ccf4b310509d5c16ea102d181630ca8 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Thu, 27 Sep 2018 22:19:26 +1000 Subject: [PATCH 03/11] Fixed type --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynalite.py | 3 ++- setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index cee5474..04dc9a8 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 1) +VERSION = (0, 1, 4) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index d72dc1d..f6c2dc1 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -269,13 +269,14 @@ def _configure(self): areaFade = self._config.area[areaValue]['fade'] if 'fade' in self._config.area[areaValue] else None if areaFade is None: areaFade = self._config.default['fade'] if 'fade' in self._config.default else 2 + areaFade = float(areaFade) if 'nodefault' in self._config.area[areaValue] and self._config.area[areaValue]['nodefault'] == True: defaultPresets = None else: defaultPresets = self._config.preset self._logger.debug( - "Generating Area '%d/%s' with a default fade of %d" % (int(areaValue), areaName, areaFade)) + "Generating Area '%d/%s' with a default fade of %f" % (int(areaValue), areaName, areaFade)) self.devices['area'][int(areaValue)] = DynaliteArea( name=areaName, value=areaValue, fade=areaFade, areaPresets=areaPresets, defaultPresets=defaultPresets, logger=self._logger, broadcastFunction=self.broadcast, dynetControl=self.control) self._configured = True diff --git a/setup.py b/setup.py index cfe9c08..87cb4f0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.1", + version="0.1.4", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", From 2dc176dba34096c1c895bf1deafc0f619a48f9c2 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 13:30:26 +1000 Subject: [PATCH 04/11] Added state request --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynalite.py | 26 ++++++++++++++++++++++++++ setup.py | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index 04dc9a8..adce9e0 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 4) +VERSION = (0, 1, 5) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index f6c2dc1..d0d2a62 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -119,6 +119,8 @@ def __init__(self, name=None, value=None, fade=2, logger=None, broadcastFunction def turnOn(self, send=True): self.active = True + if self.area: + self.area.activePreset = self.value if self.broadcastFunction: broadcastData = { 'area': self.area.value, @@ -161,6 +163,7 @@ def __init__(self, name=None, value=None, fade=2, areaPresets=None, defaultPrese self.value = int(value) self.fade = fade self.preset = {} + self.activePreset = None self.broadcastFunction = broadcastFunction self._dynetControl = dynetControl if areaPresets: @@ -281,6 +284,29 @@ def _configure(self): name=areaName, value=areaValue, fade=areaFade, areaPresets=areaPresets, defaultPresets=defaultPresets, logger=self._logger, broadcastFunction=self.broadcast, dynetControl=self.control) self._configured = True + def state(self): + self.loop.create_task(self._state()) + + @asyncio.coroutine + def _state(self): + for areaValue in self.devices['area']: + area = self.devices['area'][areaValue] + for presetValue in area: + preset = area.preset[presetValue] + presetState = 'ON' if preset.active else 'OFF' + broadcastData = { + 'area': area.value, + 'preset': preset.value, + 'name': area.name + ' ' + preset.name, + 'state': presetState + } + self.broadcast( + Event(eventType='newpreset', data=broadcastData)) + if preset.active: + self.broadcastFunction( + Event(eventType='preset', data=broadcastData)) + + def addListener(self, listenerFunction=None): broadcaster = Broadcaster( listenerFunction=listenerFunction, loop=self.loop) diff --git a/setup.py b/setup.py index 87cb4f0..2a445c8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.4", + version="0.1.5", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", From eabe07f2c575c5778384cf81bfc272a076f16fc7 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 14:21:34 +1000 Subject: [PATCH 05/11] Fixed area preset request --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynalite.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index adce9e0..4b076b6 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 5) +VERSION = (0, 1, 6) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index d0d2a62..e615de4 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -291,7 +291,7 @@ def state(self): def _state(self): for areaValue in self.devices['area']: area = self.devices['area'][areaValue] - for presetValue in area: + for presetValue in area.preset: preset = area.preset[presetValue] presetState = 'ON' if preset.active else 'OFF' broadcastData = { diff --git a/setup.py b/setup.py index 2a445c8..9ce3f80 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.5", + version="0.1.6", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", From ab8ce2b739479ae6a31ed656e8639dc8cc0f4571 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 20:06:10 +1000 Subject: [PATCH 06/11] Changed inbound data handling --- dynalite_lib/__init__.py | 4 ++ dynalite_lib/__version__.py | 2 +- dynalite_lib/const.py | 79 +++++++++++++++++++++++++++++++++++++ dynalite_lib/dynalite.py | 20 ++++------ dynalite_lib/dynet.py | 63 +++++++++++++++++------------ dynalite_lib/inbound.py | 75 +++++++++++++++++++++++++++++++++++ setup.py | 2 +- test.py | 5 +-- 8 files changed, 207 insertions(+), 43 deletions(-) create mode 100644 dynalite_lib/const.py create mode 100644 dynalite_lib/inbound.py diff --git a/dynalite_lib/__init__.py b/dynalite_lib/__init__.py index fba3d51..75b4c72 100644 --- a/dynalite_lib/__init__.py +++ b/dynalite_lib/__init__.py @@ -1,2 +1,6 @@ """Dynalite Communications""" from .dynalite import Dynalite +from .dynet import Dynet +from .dynet import DynetPacket +from .const import OpcodeType +from .inbound import DynetInbound diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index 4b076b6..bcb65c1 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 6) +VERSION = (0, 1, 8) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/const.py b/dynalite_lib/const.py new file mode 100644 index 0000000..472685b --- /dev/null +++ b/dynalite_lib/const.py @@ -0,0 +1,79 @@ +""" +@ Author : Troy Kelly +@ Date : 23 Sept 2018 +@ Description : Philips Dynalite Library - Unofficial interface for Philips Dynalite over RS485 +""" + +from enum import Enum + + +class OpcodeType(Enum): + """Types of Dyney Opcodes""" + PRESET_1 = 0 + PRESET_2 = 1 + PRESET_3 = 2 + PRESET_4 = 3 + RECALL_OFF = 4 + DECREASE_LEVEL = 5 + INCREMENT_LEVEL = 6 + ENTER_PROGRAM_MODE = 7 + PROGRAM_OUT_CURRENT_PRESET = 8 + PROGRAM_LEVELS_PRESET = 9 + PRESET_5 = 10 + PRESET_6 = 11 + PRESET_7 = 12 + PRESET_8 = 13 + RESET_TO_PRESET = 15 + DMX = 16 + PE_CONTROL = 17 + SET_PE_MIN = 18 + SET_PE_MAX = 19 + AREA_JOIN_LEVEL = 20 + LOCK_CONTROL_PANELS = 21 + ENABLE_CONTROL_PANELS = 22 + PANIC = 23 + UNPANIC = 24 + SET_PE_SPEED = 25 + DISABLE_LIGHT_LEVEL = 26 + ENABLE_LIGHT_LEVEL = 27 + DISABLE_LIGHT_LEVEL_PRESETS = 28 + ENABLE_LIGHT_LEVEL_PRESETS = 29 + DECREMENT_PRESET = 30 + INCREMENT_PRESET = 31 + SET_AREA_LINK = 32 + CLEAR_AREA_LINK = 33 + REPLY_AREA_LINK = 34 + REQUEST_AREA_LINKS = 35 + SET_FADE_TIME = 40 + SUSPEND_OCCUPANCY_PRESETS = 44 + RESUME_OCCUPANCY_PRESETS = 45 + MOTION_ACTIVITY_SYNC = 46 + OCCUPANCY_NOTIFIER = 47 + SUSPEND_OCCUPANCY_SYNC_PRESETS = 48 + SUSPEND_OCCUPANCY_DETECTION_PRESETS = 49 + DISABLE_OCCUPANCY_PRESET = 58 + ENABLE_OCCUPANCY_PRESET = 59 + DISABLE_OCCUPANCY_ALL = 60 + ENABLE_OCCUPANCY_ALL = 61 + AREA_JOIN_MASK = 64 + PANEL_LIGHTING = 72 + REQUEST_AREA_TEMP = 73 + RAMP_ALL_CHANNELS = 95 + REPORT_CHANNEL_LEVEL = 96 + REQUEST_CHANNEL_LEVEL = 97 + REPORT_PRESET = 98 + REQUEST_PRESET = 99 + PRESET_OFFSET = 100 + LINEAR_PRESET = 101 + SAVE_CURRENT_PRESET = 102 + RESTORE_CURRENT_PRESET = 103 + TURN_ALL_AREAS_OFF = 104 + TURN_ALL_AREAS_ON = 105 + TOGGLE_CHANNEL_STATE = 112 + START_FADING_FAST = 113 + START_FADING_MED = 114 + START_FADING_SLOW = 115 + STOP_FADING = 118 + START_FADING_ALL = 121 + STOP_FADING_ALL = 122 + PROGRAM_TOGGLE_PRESET = 125 diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index e615de4..bda5c52 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -11,7 +11,6 @@ import json from .dynet import Dynet, DynetControl - class BroadcasterError(Exception): def __init__(self, message): self.message = message @@ -188,6 +187,11 @@ def presetOn(self, preset, send=True): value=preset, fade=self.fade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) self.preset[preset].turnOn(send=send) + def presetOff(self, preset, send=True): + if preset not in self.preset: + self.preset[preset] = DynalitePreset( + value=preset, fade=self.fade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) + self.preset[preset].turnOff(send=send) class Dynalite(object): @@ -220,17 +224,9 @@ def processTraffic(self, event): @asyncio.coroutine def _processTraffic(self, event): - if event.eventType == 'AREAPRESET': - self.devices['area'][event.data['area'] - ].presetOn(event.data['preset'],send=False) - else: - broadcastData = { - 'area': event.data['area'], - 'namename': self.devices['area'][event.data['area']].name, - 'data': event.data.toJson() - } - self.broadcast( - Event(eventType='unknown', data=broadcastData)) + if event.eventType == 'PRESET': + self.devices['area'][event.data['area']].presetOn(event.data['preset'],send=False) + self.broadcast(event) @asyncio.coroutine def _connect(self): diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py index 743104d..502614f 100644 --- a/dynalite_lib/dynet.py +++ b/dynalite_lib/dynet.py @@ -9,6 +9,8 @@ import asyncio import logging import json +from .const import OpcodeType +from .inbound import DynetInbound LOG = logging.getLogger(__name__) @@ -26,6 +28,7 @@ def __init__(self, message): class DynetPacket(object): def __init__(self, msg=None): + self.opcodeType = None self.sync = None self.area = None self.data = [] @@ -53,8 +56,8 @@ def fromMsg(self, msg): bytes.append(int(byte)) self._msg = bytes if(len(self._msg) > 8): - self.excess = self._msg[7:] - self._msg = self._msg[:7] + self.excess = self._msg[:8] + self._msg = self._msg[8:] if self.calcsum(self._msg) != self._msg[7]: raise PacketError("Failed checksum %s" % self._msg) self.sync = self._msg[0] @@ -64,16 +67,7 @@ def fromMsg(self, msg): self.join = self._msg[6] self.chk = self._msg[7] if self.sync == 28: - if self.command < 4 or (self.command > 9 and self.command < 14): - if self.command > 3: - self.preset = self.command - 6 - else: - self.preset = self.command - self.preset = (self.preset + (self.data[2] * 8)) + 1 - self.fade = (self.data[0] + (self.data[1] * 256)) * 0.02 - if self.command == 101: - self.preset = self.data[0] + 1 - self.fade = (self.data[1] + (self.data[2] * 256)) * 0.02 + self.opcodeType = OpcodeType(self.command).name def toJson(self): return json.dumps(self.__dict__) @@ -82,6 +76,9 @@ def calcsum(self, msg): msg = msg[:7] return (-(sum(ord(c) for c in "".join(map(chr, msg))) % 256) & 0xFF) + def __repr__(self): + return json.dumps(self.__dict__) + class DynetEvent(object): @@ -160,23 +157,25 @@ def __init__(self, dynet, loop, areaDefinition=None): self._area = areaDefinition def areaPreset(self, area, preset, fade=2): - self._loop.create_task(self._areaPreset(area=area,preset=preset,fade=fade)) + self._loop.create_task(self._areaPreset( + area=area, preset=preset, fade=fade)) @asyncio.coroutine def _areaPreset(self, area, preset, fade): packet = DynetPacket() preset = preset - 1 - bank = int((preset)/8) - opcode = preset-(bank*8) + bank = int((preset) / 8) + opcode = preset - (bank * 8) if opcode > 3: opcode = opcode + 6 fadeLow = int(fade / 0.02) - (int((fade / 0.02) / 256) * 256) fadeHigh = int((fade / 0.02) / 256) - packet.toMsg(sync=28, area=area, command=opcode, data=[fadeLow, fadeHigh, bank], join=255) + packet.toMsg(sync=28, area=area, command=opcode, + data=[fadeLow, fadeHigh, bank], join=255) self._dynet.write(packet) def areaOff(self, area, fade=2): - self._loop.create_task(self._areaOff(area=area,fade=fade)) + self._loop.create_task(self._areaOff(area=area, fade=fade)) @asyncio.coroutine def _areaOff(self, area, fade): @@ -185,11 +184,11 @@ def _areaOff(self, area, fade): fade = 25.5 if fade < 0: fade = 0 - packet.toMsg(sync=28, area=area, command=104, data=[255, 0, int(fade*10)], join=255) + packet.toMsg(sync=28, area=area, command=104, data=[ + 255, 0, int(fade * 10)], join=255) self._dynet.write(packet) - class Dynet(object): def __init__(self, host=None, port=None, broadcaster=None, onConnect=None, onDisconnect=None, loop=None): @@ -212,6 +211,7 @@ def __init__(self, host=None, port=None, broadcaster=None, onConnect=None, onDis self._outBuffer = [] self._timeout = 30 + def cleanup(self): self._connection_retry_timer = 1 self._inBuffer = [] @@ -234,12 +234,22 @@ async def _connect(self): @asyncio.coroutine def _receive(self, data): - packet = DynetPacket(data) - if hasattr(packet, 'preset'): - event = DynetEvent(eventType='AREAPRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area':packet.area,'preset':packet.preset,'fade':packet.fade,'join':packet.join}, direction="IN") - self.broadcast(event) + try: + packet = DynetPacket(data) + except PacketError as err: + LOG.error(err) + LOG.info(packet) + if hasattr(packet, 'excess'): + self._loop.create_task(self._receive(packet.excess)) + if hasattr(packet, 'opcodeType'): + inboundHandler = DynetInbound() + if hasattr(inboundHandler, packet.opcodeType.lower()): + event = getattr(inboundHandler, packet.opcodeType.lower())(packet) + self.broadcast(event) + else: + LOG.debug("Unhandled Dynet Inbound (%s): %s" % (packet.opcodeType, packet)) else: - LOG.debug("Dynet Inbound: %s" % packet.toJson()) + LOG.debug("Unhandled Dynet Inbound: %s" % packet) @asyncio.coroutine def _pause(self): @@ -260,7 +270,8 @@ def _connection(self, transport=None): if not transport is None: self._transport = transport if self._onConnect is not None: - self._loop.create_task(self._onConnect(dynet=self,transport=transport)) + self._loop.create_task(self._onConnect( + dynet=self, transport=transport)) else: raise DynetError("Connected but not transport channel provided") @@ -306,4 +317,4 @@ def _write(self, newPacket=None): except: self._logger.error("Unable to write data: %s" % msg) del self._outBuffer[idx] - #self._loop.create_task(self._receive(msg)) + # self._loop.create_task(self._receive(msg)) diff --git a/dynalite_lib/inbound.py b/dynalite_lib/inbound.py new file mode 100644 index 0000000..2ce67d5 --- /dev/null +++ b/dynalite_lib/inbound.py @@ -0,0 +1,75 @@ +""" +@ Author : Troy Kelly +@ Date : 23 Sept 2018 +@ Description : Philips Dynalite Library - Unofficial interface for Philips Dynalite over RS485 + +@ Notes: Requires a RS485 to IP gateway (Do not use the Dynalite one - use something cheaper) +""" + +import json + +class DynetEvent(object): + + def __init__(self, eventType=None, message=None, data={}, direction=None): + self.eventType = eventType.upper() if eventType else None + self.msg = message + self.data = data + self.direction = direction + + def toJson(self): + return json.dumps(self.__dict__) + + def __repr__(self): + return json.dumps(self.__dict__) + + +class DynetInbound(object): + + def __init__(self): + self._logger = None + + def preset(self, packet): + if packet.command > 3: + packet.preset = packet.command - 6 + else: + packet.preset = packet.command + packet.preset = (packet.preset + (packet.data[2] * 8)) + 1 + packet.fade = (packet.data[0] + (packet.data[1] * 256)) * 0.02 + return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join}, direction="IN") + + def preset_1(self, packet): + return self.preset(packet) + + def preset_2(self, packet): + return self.preset(packet) + + def preset_3(self, packet): + return self.preset(packet) + + def preset_4(self, packet): + return self.preset(packet) + + def preset_5(self, packet): + return self.preset(packet) + + def preset_6(self, packet): + return self.preset(packet) + + def preset_7(self, packet): + return self.preset(packet) + + def preset_8(self, packet): + return self.preset(packet) + + def request_preset(self, packet): + return DynetEvent(eventType='REQPRESET', message=("Request Area %d preset" % (packet.area)), data={'area': packet.area, 'join': packet.join}, direction="IN") + + def report_preset(self, packet): + packet.preset = packet.data[0] + 1 + # packet.fade = (packet.data[1] + (packet.data[2] * 256)) * 0.02 + return DynetEvent(eventType='PRESET', message=("Current Area %d Preset is %d" % (packet.area, packet.preset)), data={'area': packet.area, 'preset': packet.preset, 'join': packet.join}, direction="IN") + + def linear_preset(self, packet): + packet.preset = packet.data[0] + 1 + packet.fade = (packet.data[1] + (packet.data[2] * 256)) * 0.02 + return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join}, direction="IN") diff --git a/setup.py b/setup.py index 9ce3f80..f0ce01d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.6", + version="0.1.8", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", diff --git a/test.py b/test.py index 855cbea..e4ec728 100755 --- a/test.py +++ b/test.py @@ -13,15 +13,14 @@ loop = asyncio.get_event_loop() dynalite = None - def handleEvent(event=None, dynalite=None): #LOG.info("Received Event: %s" % event.eventType) LOG.debug(event.toJson()) def handleConnect(event=None, dynalite=None): - LOG.warning("Connected to Dynalite") - dynalite.devices['area'][2].preset[1].turnOn() + LOG.info("Connected to Dynalite") + dynalite.devices['area'][8].preset[10].turnOn() if __name__ == '__main__': From 0aa202b2463f0aaaf19f3b19567cdc0cc1b04d59 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 20:20:25 +1000 Subject: [PATCH 07/11] Added request current preset --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynalite.py | 1 + dynalite_lib/dynet.py | 9 +++++++++ setup.py | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index bcb65c1..fc993c3 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 8) +VERSION = (0, 1, 9) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index bda5c52..667ed10 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -301,6 +301,7 @@ def _state(self): if preset.active: self.broadcastFunction( Event(eventType='preset', data=broadcastData)) + self.control.areaReqPreset(area.value) def addListener(self, listenerFunction=None): diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py index 502614f..9f7f17f 100644 --- a/dynalite_lib/dynet.py +++ b/dynalite_lib/dynet.py @@ -188,6 +188,15 @@ def _areaOff(self, area, fade): 255, 0, int(fade * 10)], join=255) self._dynet.write(packet) + def areaReqPreset(self, area): + self._loop.create_task(self._areaReqPreset(area=area)) + + @asyncio.coroutine + def _areaReqPreset(self, area): + packet = DynetPacket() + packet.toMsg(sync=28, area=area, command=OpcodeType.REPORT_PRESET.value, data=[0, 0, 0], join=255) + self._dynet.write(packet) + class Dynet(object): diff --git a/setup.py b/setup.py index f0ce01d..fda1a68 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.8", + version="0.1.9", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", From b85d6299a6dc873bc9a717d1eb87d1fecd23d400 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 20:29:51 +1000 Subject: [PATCH 08/11] Fixed incorrect opcode --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynet.py | 2 +- setup.py | 2 +- test.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index fc993c3..2c8a275 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 9) +VERSION = (0, 1, 10) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py index 9f7f17f..b0132c7 100644 --- a/dynalite_lib/dynet.py +++ b/dynalite_lib/dynet.py @@ -194,7 +194,7 @@ def areaReqPreset(self, area): @asyncio.coroutine def _areaReqPreset(self, area): packet = DynetPacket() - packet.toMsg(sync=28, area=area, command=OpcodeType.REPORT_PRESET.value, data=[0, 0, 0], join=255) + packet.toMsg(sync=28, area=area, command=OpcodeType.REQUEST_PRESET.value, data=[0, 0, 0], join=255) self._dynet.write(packet) diff --git a/setup.py b/setup.py index fda1a68..f69271b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.9", + version="0.1.10", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface", diff --git a/test.py b/test.py index e4ec728..551ff24 100755 --- a/test.py +++ b/test.py @@ -20,7 +20,8 @@ def handleEvent(event=None, dynalite=None): def handleConnect(event=None, dynalite=None): LOG.info("Connected to Dynalite") - dynalite.devices['area'][8].preset[10].turnOn() + #dynalite.devices['area'][8].preset[10].turnOn() + dynalite.state() if __name__ == '__main__': From 267e7c83ab73098d0ef165b3c1f1e774a18a22d5 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Fri, 28 Sep 2018 21:18:22 +1000 Subject: [PATCH 09/11] Added sending throttle --- dynalite_lib/dynet.py | 74 +++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py index b0132c7..1642e8b 100644 --- a/dynalite_lib/dynet.py +++ b/dynalite_lib/dynet.py @@ -9,6 +9,7 @@ import asyncio import logging import json +import time from .const import OpcodeType from .inbound import DynetInbound @@ -194,7 +195,8 @@ def areaReqPreset(self, area): @asyncio.coroutine def _areaReqPreset(self, area): packet = DynetPacket() - packet.toMsg(sync=28, area=area, command=OpcodeType.REQUEST_PRESET.value, data=[0, 0, 0], join=255) + packet.toMsg(sync=28, area=area, command=OpcodeType.REQUEST_PRESET.value, data=[ + 0, 0, 0], join=255) self._dynet.write(packet) @@ -220,6 +222,9 @@ def __init__(self, host=None, port=None, broadcaster=None, onConnect=None, onDis self._outBuffer = [] self._timeout = 30 + self._lastSent = None + self._messageDelay = 200 + self._sending = False def cleanup(self): self._connection_retry_timer = 1 @@ -253,10 +258,12 @@ def _receive(self, data): if hasattr(packet, 'opcodeType'): inboundHandler = DynetInbound() if hasattr(inboundHandler, packet.opcodeType.lower()): - event = getattr(inboundHandler, packet.opcodeType.lower())(packet) + event = getattr( + inboundHandler, packet.opcodeType.lower())(packet) self.broadcast(event) else: - LOG.debug("Unhandled Dynet Inbound (%s): %s" % (packet.opcodeType, packet)) + LOG.debug("Unhandled Dynet Inbound (%s): %s" % + (packet.opcodeType, packet)) else: LOG.debug("Unhandled Dynet Inbound: %s" % packet) @@ -306,24 +313,43 @@ def _write(self, newPacket=None): if newPacket is not None: self._outBuffer.append(newPacket) - if self._paused: - LOG.info("Connection paused - queuing packet") - self._loop.call_later(1, self.updateLocations) - - for idx, packet in enumerate(self._outBuffer): - msg = bytearray() - msg.append(packet.sync) - msg.append(packet.area) - msg.append(packet.data[0]) - msg.append(packet.command) - msg.append(packet.data[1]) - msg.append(packet.data[2]) - msg.append(packet.join) - msg.append(packet.chk) - try: - self._transport.write(msg) - LOG.debug("Dynet Sent: %s" % msg) - except: - self._logger.error("Unable to write data: %s" % msg) - del self._outBuffer[idx] - # self._loop.create_task(self._receive(msg)) + if self._paused or self._sending: + LOG.info("Connection busy - queuing packet") + self._loop.call_later(1, self.write) + return + + if self._lastSent is None: + self._lastSent = int(round(time.time() * 1000)) + + current_milli_time = int(round(time.time() * 1000)) + elapsed = (current_milli_time - self._lastSent) + delay = (0 - (elapsed - self._messageDelay)) + if delay > 0: + self._loop.call_later(delay / 1000, self.write) + return + + if len(self._outBuffer) == 0: + return + + self._sending = True + packet = self._outBuffer[0] + msg = bytearray() + msg.append(packet.sync) + msg.append(packet.area) + msg.append(packet.data[0]) + msg.append(packet.command) + msg.append(packet.data[1]) + msg.append(packet.data[2]) + msg.append(packet.join) + msg.append(packet.chk) + try: + self._transport.write(msg) + LOG.debug("Dynet Sent: %s" % msg) + except: + self._logger.error("Unable to write data: %s" % msg) + del self._outBuffer[0] + self._lastSent = int(round(time.time() * 1000)) + self._sending = False + + if len(self._outBuffer) > 0: + self._loop.call_later(self._messageDelay / 1000, self.write) From 8ab47ee6d03c5019f38b769c30ba3175513cfa28 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Sat, 29 Sep 2018 15:19:46 +1000 Subject: [PATCH 10/11] Better packet handling --- dynalite_lib/const.py | 13 +++++++++ dynalite_lib/dynet.py | 66 ++++++++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/dynalite_lib/const.py b/dynalite_lib/const.py index 472685b..198f5cd 100644 --- a/dynalite_lib/const.py +++ b/dynalite_lib/const.py @@ -6,6 +6,15 @@ from enum import Enum +class SyncType(Enum): + """Types of Sync Code""" + LOGICAL = 28 + DEVICE = 92 + + @classmethod + def has_value(cls, value): + return any(value == item.value for item in cls) + class OpcodeType(Enum): """Types of Dyney Opcodes""" @@ -77,3 +86,7 @@ class OpcodeType(Enum): START_FADING_ALL = 121 STOP_FADING_ALL = 122 PROGRAM_TOGGLE_PRESET = 125 + + @classmethod + def has_value(cls, value): + return any(value == item.value for item in cls) diff --git a/dynalite_lib/dynet.py b/dynalite_lib/dynet.py index 1642e8b..8d1feaf 100644 --- a/dynalite_lib/dynet.py +++ b/dynalite_lib/dynet.py @@ -10,7 +10,7 @@ import logging import json import time -from .const import OpcodeType +from .const import OpcodeType, SyncType from .inbound import DynetInbound LOG = logging.getLogger(__name__) @@ -52,15 +52,15 @@ def toMsg(self, sync=28, area=0, command=0, data=[0, 0, 0], join=255): self.fromMsg(bytes) def fromMsg(self, msg): - bytes = [] - for byte in msg: - bytes.append(int(byte)) - self._msg = bytes - if(len(self._msg) > 8): - self.excess = self._msg[:8] - self._msg = self._msg[8:] - if self.calcsum(self._msg) != self._msg[7]: - raise PacketError("Failed checksum %s" % self._msg) + messageLength = len(msg) + if messageLength < 8: + raise PacketError("Message too short (%d bytes): %s" % (len(msg), msg)) + + if messageLength > 8: + raise PacketError("Message too long (%d bytes): %s" % (len(msg), msg)) + + self._msg = msg + self.sync = self._msg[0] self.area = self._msg[1] self.data = [self._msg[2], self._msg[4], self._msg[5]] @@ -68,7 +68,8 @@ def fromMsg(self, msg): self.join = self._msg[6] self.chk = self._msg[7] if self.sync == 28: - self.opcodeType = OpcodeType(self.command).name + if OpcodeType.has_value(self.command): + self.opcodeType = OpcodeType(self.command).name def toJson(self): return json.dumps(self.__dict__) @@ -247,15 +248,37 @@ async def _connect(self): self._connection_retry_timer if self._connection_retry_timer < 32 else 60 @asyncio.coroutine - def _receive(self, data): - try: - packet = DynetPacket(data) - except PacketError as err: - LOG.error(err) - LOG.info(packet) - if hasattr(packet, 'excess'): - self._loop.create_task(self._receive(packet.excess)) - if hasattr(packet, 'opcodeType'): + def _receive(self, data=None): + if data is not None: + for byte in data: + self._inBuffer.append(int(byte)) + + inBufferLength = len(self._inBuffer) + if inBufferLength < 8: + LOG.debug("Received %d bytes, not enough to process: %s" %(inBufferLength,self._inBuffer)) + + packet = None + while inBufferLength >= 8 and packet is None: + if SyncType.has_value(self._inBuffer[0]): + try: + packet = DynetPacket(msg=self._inBuffer[:8]) + except PacketError as err: + LOG.error(err) + packet = None + if packet is None: + del self._inBuffer[0] + inBufferLength = len(self._inBuffer) + else: + self._inBuffer = self._inBuffer[8:] + inBufferLength = len(self._inBuffer) + + if packet is None: + LOG.debug("Unable to find message in buffer") + return + else: + LOG.debug("Have packet: %s" % packet) + + if hasattr(packet, 'opcodeType') and packet.opcodeType is not None: inboundHandler = DynetInbound() if hasattr(inboundHandler, packet.opcodeType.lower()): event = getattr( @@ -266,6 +289,9 @@ def _receive(self, data): (packet.opcodeType, packet)) else: LOG.debug("Unhandled Dynet Inbound: %s" % packet) + # If there is still buffer to process - start again + if inBufferLength >= 8: + self._loop.create_task(self._receive()) @asyncio.coroutine def _pause(self): From 92db60092e6aa32621dada564a23bcad9b210ea3 Mon Sep 17 00:00:00 2001 From: Troy Kelly Date: Sun, 30 Sep 2018 10:17:50 +1000 Subject: [PATCH 11/11] Fixed incoming dynet traffic handling --- dynalite_lib/__version__.py | 2 +- dynalite_lib/dynalite.py | 26 +++++++++++++------------- dynalite_lib/inbound.py | 8 ++++---- setup.py | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dynalite_lib/__version__.py b/dynalite_lib/__version__.py index 2c8a275..1f9c000 100644 --- a/dynalite_lib/__version__.py +++ b/dynalite_lib/__version__.py @@ -1,4 +1,4 @@ """Version of package.""" -VERSION = (0, 1, 10) +VERSION = (0, 1, 12) __version__ = '.'.join(map(str, VERSION)) diff --git a/dynalite_lib/dynalite.py b/dynalite_lib/dynalite.py index 667ed10..2e3147d 100644 --- a/dynalite_lib/dynalite.py +++ b/dynalite_lib/dynalite.py @@ -116,11 +116,11 @@ def __init__(self, name=None, value=None, fade=2, logger=None, broadcastFunction self.broadcastFunction( Event(eventType='newpreset', data=broadcastData)) - def turnOn(self, send=True): + def turnOn(self, sendDynet=True, sendMQTT=True): self.active = True if self.area: self.area.activePreset = self.value - if self.broadcastFunction: + if sendMQTT and self.broadcastFunction: broadcastData = { 'area': self.area.value, 'preset': self.value, @@ -129,17 +129,17 @@ def turnOn(self, send=True): } self.broadcastFunction( Event(eventType='preset', data=broadcastData)) - if send and self._control: + if sendDynet and self._control: self._control.areaPreset( area=self.area.value, preset=self.value, fade=self.fade) for preset in self.area.preset: if self.value != preset: if self.area.preset[preset].active: - self.area.preset[preset].turnOff(send=False) + self.area.preset[preset].turnOff(sendDynet=False, sendMQTT=True) - def turnOff(self, send=True): + def turnOff(self, sendDynet=True, sendMQTT=True): self.active = False - if self.broadcastFunction: + if sendMQTT and self.broadcastFunction: broadcastData = { 'area': self.area.value, 'preset': self.value, @@ -148,7 +148,7 @@ def turnOff(self, send=True): } self.broadcastFunction( Event(eventType='preset', data=broadcastData)) - if send and self._control: + if sendDynet and self._control: self._control.areaOff(area=self.area.value, fade=self.fade) @@ -181,17 +181,17 @@ def __init__(self, name=None, value=None, fade=2, areaPresets=None, defaultPrese self.preset[int(presetValue)] = DynalitePreset( name=presetName, value=presetValue, fade=presetFade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) - def presetOn(self, preset, send=True): + def presetOn(self, preset, sendDynet=True, sendMQTT=True): if preset not in self.preset: self.preset[preset] = DynalitePreset( value=preset, fade=self.fade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) - self.preset[preset].turnOn(send=send) + self.preset[preset].turnOn(sendDynet=sendDynet, sendMQTT=sendMQTT) - def presetOff(self, preset, send=True): + def presetOff(self, preset, sendDynet=True, sendMQTT=True): if preset not in self.preset: self.preset[preset] = DynalitePreset( value=preset, fade=self.fade, logger=self._logger, broadcastFunction=self.broadcastFunction, area=self, dynetControl=self._dynetControl) - self.preset[preset].turnOff(send=send) + self.preset[preset].turnOff(sendDynet=sendDynet, sendMQTT=sendMQTT) class Dynalite(object): @@ -224,9 +224,9 @@ def processTraffic(self, event): @asyncio.coroutine def _processTraffic(self, event): - if event.eventType == 'PRESET': - self.devices['area'][event.data['area']].presetOn(event.data['preset'],send=False) self.broadcast(event) + if event.eventType == 'PRESET': + self.devices['area'][event.data['area']].presetOn(event.data['preset'],sendDynet=False, sendMQTT=False) @asyncio.coroutine def _connect(self): diff --git a/dynalite_lib/inbound.py b/dynalite_lib/inbound.py index 2ce67d5..76c3f04 100644 --- a/dynalite_lib/inbound.py +++ b/dynalite_lib/inbound.py @@ -8,6 +8,7 @@ import json + class DynetEvent(object): def __init__(self, eventType=None, message=None, data={}, direction=None): @@ -35,7 +36,7 @@ def preset(self, packet): packet.preset = packet.command packet.preset = (packet.preset + (packet.data[2] * 8)) + 1 packet.fade = (packet.data[0] + (packet.data[1] * 256)) * 0.02 - return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join}, direction="IN") + return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join, 'state': 'ON'}, direction="IN") def preset_1(self, packet): return self.preset(packet) @@ -66,10 +67,9 @@ def request_preset(self, packet): def report_preset(self, packet): packet.preset = packet.data[0] + 1 - # packet.fade = (packet.data[1] + (packet.data[2] * 256)) * 0.02 - return DynetEvent(eventType='PRESET', message=("Current Area %d Preset is %d" % (packet.area, packet.preset)), data={'area': packet.area, 'preset': packet.preset, 'join': packet.join}, direction="IN") + return DynetEvent(eventType='PRESET', message=("Current Area %d Preset is %d" % (packet.area, packet.preset)), data={'area': packet.area, 'preset': packet.preset, 'join': packet.join, 'state': 'ON'}, direction="IN") def linear_preset(self, packet): packet.preset = packet.data[0] + 1 packet.fade = (packet.data[1] + (packet.data[2] * 256)) * 0.02 - return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join}, direction="IN") + return DynetEvent(eventType='PRESET', message=("Area %d Preset %d Fade %d seconds." % (packet.area, packet.preset, packet.fade)), data={'area': packet.area, 'preset': packet.preset, 'fade': packet.fade, 'join': packet.join, 'state': 'ON'}, direction="IN") diff --git a/setup.py b/setup.py index f69271b..0200e71 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="dynalite", - version="0.1.10", + version="0.1.12", author="Troy Kelly", author_email="troy@troykelly.com", description="An unofficial Dynalite DyNET interface",