Skip to content

Commit

Permalink
Add Groups and value expiration
Browse files Browse the repository at this point in the history
Support for groups
Add expiration for occupancy cluster and special multiclick onoff cluster (xiaomi)
  • Loading branch information
doudz committed Mar 15, 2018
1 parent 39ebd53 commit 49e0de0
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 26 deletions.
2 changes: 1 addition & 1 deletion zigate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .const import *
from pydispatch import dispatcher

__version__ = '0.10.7'
__version__ = '0.11.0'

__all__ = ['ZiGate', 'ZiGateWiFi',
'dispatcher']
27 changes: 21 additions & 6 deletions zigate/clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from binascii import unhexlify, hexlify
import logging


LOGGER = logging.getLogger('zigate')


Expand Down Expand Up @@ -72,14 +71,14 @@ def update(self, data):
attribute.update(attr_def)
try:
attribute['value'] = eval(attribute['value'],
globals(),
{'value': attribute['data']})
globals(),
{'value': attribute['data']})
except:
LOGGER.error('Failed to eval "{}" using "{}"'.format(attribute['value'],
attribute['data']
))
attribute['value'] = None
return added
return (added, attribute)

def __str__(self):
return 'Cluster 0x{:04x} {}'.format(self.cluster_id, self.type)
Expand All @@ -103,6 +102,20 @@ def from_json(data):
cluster.update(attribute)
return cluster

def get_property(self, name):
'''
return attribute matching name
'''
for attribute in self.attributes.values():
if attribute.get('name') == name:
return attribute

def has_property(self, name):
'''
check attribute matching name exist
'''
return self.get_property(name) is not None


@register_cluster
class C0000(Cluster):
Expand Down Expand Up @@ -148,7 +161,7 @@ class C0006(Cluster):
cluster_id = 0x0006
type = 'General: On/Off'
attributes_def = {0x0000: {'name': 'onoff', 'value': 'value'},
0x8000: {'name': 'multiclick', 'value': 'value'},
0x8000: {'name': 'multiclick', 'value': 'value', 'expire': 1},
}


Expand Down Expand Up @@ -205,5 +218,7 @@ class C0405(Cluster):
class C0406(Cluster):
cluster_id = 0x0406
type = 'Measurement: Occupancy Sensing'
attributes_def = {0x0000: {'name': 'presence', 'value': 'value'},
attributes_def = {0x0000: {'name': 'presence', 'value': 'value', 'expire': 10},
}


122 changes: 105 additions & 17 deletions zigate/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import struct
import threading
import random
from enum import Enum


LOGGER = logging.getLogger('zigate')

Expand All @@ -23,6 +25,18 @@
BIND_REPORT_LIGHT = True # automatically bind and report state for light
ACTIONS = {}

# Device id
ACTUATORS = [0x0010, 0x0051,
0x0100, 0x0110,
0x0200, 0x0210, 0x0220]
# On/off light 0x0000
# On/off plug-in unit 0x0010
# Dimmable light 0x0100
# Dimmable plug-in unit 0x0110
# Color light 0x0200
# Extended color light 0x0210
# Color temperature light 0x0220


def register_actions(action):
def decorator(func):
Expand All @@ -33,6 +47,13 @@ def decorator(func):
return decorator


class AddrMode(Enum):
bound = 0
group = 1
short = 2
ieee = 3


class ZiGate(object):

def __init__(self, port='auto', path='~/.zigate.json',
Expand Down Expand Up @@ -80,8 +101,11 @@ def save_state(self, path=None):
path = path or self._path
self._path = os.path.expanduser(path)
try:
data = {'devices': list(self._devices.values()),
'groups': self._groups
}
with open(self._path, 'w') as fp:
json.dump(list(self._devices.values()), fp, cls=DeviceEncoder,
json.dump(data, fp, cls=DeviceEncoder,
sort_keys=True, indent=4, separators=(',', ': '))
except Exception as e:
LOGGER.error('Failed to save persistent file {}'.format(self._path))
Expand All @@ -95,12 +119,15 @@ def load_state(self, path=None):
if os.path.exists(self._path):
try:
with open(self._path) as fp:
devices = json.load(fp)
data = json.load(fp)
if not isinstance(data, dict): # old version
data = {'devices': data, 'groups': {}}
self._groups = data.get('groups', {})
devices = data.get('devices', [])
for data in devices:
device = Device.from_json(data, self)
self._devices[device.addr] = device
device._create_actions()
# device._bind_report()
LOGGER.debug('Load success')
return True
except Exception as e:
Expand Down Expand Up @@ -137,7 +164,9 @@ def autoStart(self):
LOGGER.debug('Check network state')
self.start_network()
network_state = self.get_network_state()
if network_state.get('extend_pan') == 0:
if not network_state:
LOGGER.error('Failed to get network state')
if not network_state or network_state.get('extend_pan') == 0:
LOGGER.debug('Network is down, start it')
self.start_network(True)
self.get_devices_list(True)
Expand Down Expand Up @@ -248,7 +277,7 @@ def decode_data(self, packet):
struct.unpack('!HHB%dsB' % (len(decoded) - 6), decoded)
if length != len(value)+1: # add rssi length
LOGGER.error('Bad length {} != {} : {}'.format(length,
len(value),
len(value)+1,
value))
return
computed_checksum = self.checksum(decoded[:4], rssi, value)
Expand Down Expand Up @@ -297,7 +326,7 @@ def interpret_response(self, response):
ep['in_clusters'] = response['in_clusters']
ep['out_clusters'] = response['out_clusters']
d._create_actions()
d._bind_report()
d._bind_report(endpoint)
# ask for various general information
for c in response['in_clusters']:
cluster = CLUSTERS.get(c)
Expand All @@ -318,10 +347,14 @@ def interpret_response(self, response):
return
device = self._get_device(response['addr'])
device.rssi = response['rssi']
added = device.set_attribute(response['endpoint'], response['cluster'], response.cleaned_data())
added = device.set_attribute(response['endpoint'],
response['cluster'],
response.cleaned_data())
if added is None:
return
changed = device.get_attribute(response['endpoint'], response['cluster'], response['attribute'], True)
changed = device.get_attribute(response['endpoint'],
response['cluster'],
response['attribute'], True)
if added:
LOGGER.debug('Dispatch ZIGATE_ATTRIBUTE_ADDED')
dispatcher.send(ZIGATE_ATTRIBUTE_ADDED, self, **{'zigate': self,
Expand Down Expand Up @@ -564,10 +597,15 @@ def _bind_unbind(self, cmd, ieee, endpoint, cluster,
if not dst_addr:
dst_addr = self.ieee
if len(dst_addr) == 4:
dst_addr_mode = 2
if dst_addr in self._groups:
dst_addr_mode = 1 # AddrMode.group
elif dst_addr in self._devices:
dst_addr_mode = 2 # AddrMode.short
else:
dst_addr_mode = 0 # AddrMode.bound
dst_addr_fmt = 'H'
else:
dst_addr_mode = 1
dst_addr_mode = 3 # AddrMode.ieee
dst_addr_fmt = 'Q'
ieee = self.__addr(ieee)
dst_addr = self.__addr(dst_addr)
Expand Down Expand Up @@ -881,7 +919,7 @@ def attribute_discovery_request(self, addr, endpoint, cluster):
manufacturer_specific = 0
manufacturer_id = 0
data = struct.pack('!BHBBHBBBHB', 2, addr, 1, endpoint, cluster,
0, direction, manufacturer_specific,
0, direction, manufacturer_specific,
manufacturer_id, 255)
self.send_data(0x0140, data)

Expand Down Expand Up @@ -990,6 +1028,13 @@ def __init__(self, host, port=9999, path='~/.zigate.json',
def setup_connection(self):
self.connection = ThreadSocketConnection(self, self._host, self._port)

def reboot(self):
'''
ask zigate wifi to reboot
'''
import requests
r = requests.get('http://{}/reboot'.format(self._host))


class DeviceEncoder(json.JSONEncoder):
def default(self, obj):
Expand All @@ -1008,6 +1053,7 @@ def __init__(self, info=None, zigate_instance=None):
self._lock = threading.Lock()
self.info = info or {}
self.endpoints = {}
self._expire_timer = {}

def available_actions(self, endpoint_id=None):
'''
Expand All @@ -1027,7 +1073,7 @@ def available_actions(self, endpoint_id=None):
actions[ep_id] = []
endpoint = self.endpoints.get(ep_id)
if endpoint:
if endpoint['device'] in [0x0002, 0x0100, 0x0051, 0x0210]: # known device id that support onoff
if endpoint['device'] in ACTUATORS:
if 0x0006 in endpoint['in_clusters']:
actions[ep_id].append('onoff')
if 0x0008 in endpoint['in_clusters']:
Expand All @@ -1049,15 +1095,18 @@ def _create_actions(self):
functools.update_wrapper(wfunc, func)
setattr(self, func_name, wfunc)

def _bind_report(self):
def _bind_report(self, enpoint_id=None):
'''
automatically bind and report data for light
'''
if not BIND_REPORT_LIGHT:
return
for endpoint_id, endpoint in self.endpoints.items():
if endpoint['device'] == 0x0100: # light
ieee = self.info['ieee']
if enpoint_id:
endpoints_list = [(enpoint_id, self.endpoints[enpoint_id])]
else:
endpoints_list = self.endpoints.items()
for endpoint_id, endpoint in endpoints_list:
if endpoint['device'] in ACTUATORS: # light
if 0x0006 in endpoint['in_clusters']:
LOGGER.debug('bind and report for cluster 0x0006')
self._zigate.bind(self.ieee, endpoint_id, 0x0006)
Expand Down Expand Up @@ -1235,10 +1284,49 @@ def set_attribute(self, endpoint_id, cluster_id, data):
self.info['last_seen'] = strftime('%Y-%m-%d %H:%M:%S')
cluster = self.get_cluster(endpoint_id, cluster_id)
self._lock.acquire()
added = cluster.update(data)
added, attribute = cluster.update(data)
if 'expire' in attribute:
self._set_expire_timer(endpoint_id, cluster_id,
attribute['attribute'], attribute['expire'])
self._lock.release()
return added

def _set_expire_timer(self, endpoint_id, cluster_id, attribute_id, expire):
LOGGER.debug('Set expire timer for {}-{}-{} in {}'.format(endpoint_id,
cluster_id,
attribute_id,
expire))
k = (endpoint_id, cluster_id, attribute_id)
timer = self._expire_timer.get(k)
if timer:
timer.cancel()
timer = threading.Timer(expire,
functools.partial(self._reset_attribute,
endpoint_id,
cluster_id,
attribute_id))
timer.setDaemon(True)
timer.start()

def _reset_attribute(self, endpoint_id, cluster_id, attribute_id,
new_value=None):
attribute = self.get_attribute(endpoint_id,
cluster_id,
attribute_id)
value = attribute['value']
if new_value is None:
new_value = type(value)()
attribute['value'] = new_value
attribute = self.get_attribute(endpoint_id,
cluster_id,
attribute_id,
True)
LOGGER.debug('Dispatch ZIGATE_ATTRIBUTE_UPDATED (auto reset)')
dispatcher.send(ZIGATE_ATTRIBUTE_UPDATED, self._zigate,
**{'zigate': self._zigate,
'device': self,
'attribute': attribute})

def get_attribute(self, endpoint_id, cluster_id, attribute_id, extended_info=False):
if endpoint_id in self.endpoints:
endpoint = self.endpoints[endpoint_id]
Expand Down
4 changes: 2 additions & 2 deletions zigate/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def close(self):
self.serial.close()


class ThreadSocketConnection_old(ThreadSerialConnection):
class ThreadSocketConnection2(ThreadSerialConnection):
def __init__(self, device, host, port=9999):
self._host = host
ThreadSerialConnection.__init__(self, device, port)
Expand All @@ -103,7 +103,7 @@ def __init__(self, device, host, port=9999):
ThreadSerialConnection.__init__(self, device, port)

def initSerial(self):
return socket.create_connection((self._host, self._port), timeout=0.05)
return socket.create_connection((self._host, self._port), timeout=0.1)

def listen(self):
while self._running:
Expand Down

0 comments on commit 49e0de0

Please sign in to comment.