Skip to content
This repository has been archived by the owner on Oct 5, 2024. It is now read-only.

Commit

Permalink
Merge branch 'dev', release v0.40.0
Browse files Browse the repository at this point in the history
  • Loading branch information
doudz committed Jul 1, 2020
2 parents e80712c + a97e3fc commit 30a8a5d
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 49 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package

on:
release:
types: [created]

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist
twine upload dist/*
37 changes: 37 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Build & Tests

on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -e ".[dev]"
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# zigate

[![Build Status](https://travis-ci.com/doudz/zigate.svg?branch=master)](https://travis-ci.com/doudz/zigate)
![Build & Tests](https://github.com/doudz/zigate/workflows/Build%20&%20Tests/badge.svg)
[![PyPI version](https://badge.fury.io/py/zigate.svg)](https://pypi.python.org/pypi/zigate)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/doudz/zigate.svg)](http://isitmaintained.com/project/doudz/zigate "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/doudz/zigate.svg)](http://isitmaintained.com/project/doudz/zigate "Percentage of issues still open")
Expand Down
47 changes: 42 additions & 5 deletions tests/test_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,27 @@
import unittest
from zigate import clusters, core
import json
from binascii import unhexlify


class TestResponses(unittest.TestCase):
def test_xiaomi_struct(self):
# lumi.weather
rawdata = unhexlify(b'0121bd0b0421a81305210e0006240100000000642971086521610f662ba58201000a210000')
rawdata = b'0121bd0b0421a81305210e0006240100000000642971086521610f662ba58201000a210000'
data = clusters.decode_xiaomi(rawdata)
self.assertEqual(data[1], 3005) # battery
self.assertEqual(data[100], 2161) # temperature
self.assertEqual(data[101], 3937) # humidity
self.assertEqual(data[102], 98981) # pressure

# lumi magnet sensor
rawdata = unhexlify(b'0121030c0328100421a81305211f00062401000000000a210000')
rawdata = b'0121030c0328100421a81305211f00062401000000000a210000'
data = clusters.decode_xiaomi(rawdata)

# aqara bulb
rawdata = unhexlify(b'03283c0521a4000727000000000000000008211601092100010a2100006420016520fe6621d901')
rawdata = b'03283c0521a4000727000000000000000008211601092100010a2100006420016520fe6621d901'
data = clusters.decode_xiaomi(rawdata)
self.assertEqual(data[100], 1) # ON
rawdata = unhexlify(b'03283c0521a4000727000000000000000008211601092100010a2100006420006520fe6621d901')
rawdata = b'03283c0521a4000727000000000000000008211601092100010a2100006420006520fe6621d901'
data = clusters.decode_xiaomi(rawdata)
self.assertEqual(data[100], 0) # OFF

Expand Down Expand Up @@ -151,6 +150,44 @@ def test_cluster_C0000(self):
'"type": "str", "value": "test.test"}], "cluster": 0}'))

endpoint = {'device': 1}
data = {"attributes": [{"attribute": 65281,
"data": '0121a90b03281f0421a84305211c00062429000a01030a2100006410000b219b00',
}],
"cluster": 0
}
c = clusters.C0000.from_json(data, endpoint)
self.assertEqual(c.attributes,
{65281: {'attribute': 65281,
'data': '0121a90b03281f0421a84305211c00062429000a01030a2100006410000b219b00',
'name': 'xiaomi', 'value': {1: 2985,
3: 31,
4: 17320,
5: 28,
6: b')\x00\n\x01\x03',
10: 0,
11: 155,
100: False}, 'type': dict}}
)
endpoint = {'device': 1}
data = {"attributes": [{"attribute": 65281,
"data": '0121130b0421a84305211300062401000000006429ed0965219513662be18201000a210000',
}],
"cluster": 0
}
c = clusters.C0000.from_json(data, endpoint)
self.assertEqual(c.attributes,
{65281: {'attribute': 65281,
'data': '0121130b0421a84305211300062401000000006429ed0965219513662be18201000a210000',
'name': 'xiaomi', 'value': {1: 2835,
4: 17320,
5: 19,
6: b'\x01\x00\x00\x00\x00',
100: 2541,
101: 5013,
102: 99041,
10: 0}, 'type': dict}}
)
endpoint = {'device': 1}
data = {"attributes": [{"attribute": 65282,
"data": '100121e50b21a801240000000000217c012067',
}],
Expand Down
33 changes: 22 additions & 11 deletions tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,18 @@ def test_template(self):
device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'})
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.test'})
self.assertFalse(device.load_template())
self.assertNotEqual(device.discovery, 'templated')

device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'})
self.assertFalse(device.load_template())
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.weather'})

self.assertCountEqual(device.properties,
[{'attribute': 5, 'data': 'lumi.weather',
'name': 'type', 'value': 'lumi.weather', 'type': str}]
)
self.assertEqual(device.get_type(), 'lumi.weather')

device.set_attribute(1, 0x0402, {'attribute': 0, 'lqi': 255, 'data': 1200})
self.assertEqual(device.get_property_value('temperature'), 12.0)
device.set_attribute(1, 0, {'attribute': 1, 'lqi': 255, 'data': 'test'})
self.assertEqual(device.genericType, '')
# self.assertEqual(device.genericType, '')
self.assertTrue(device.load_template())
self.assertEqual(device.discovery, 'templated')
self.assertEqual(device.genericType, 'sensor')
Expand Down Expand Up @@ -121,7 +120,7 @@ def test_template(self):

device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'})
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.sensor_cube'})
self.assertTrue(device.load_template())
self.assertEqual(device.discovery, 'templated')
self.assertCountEqual(device.attributes,
[{'endpoint': 1, 'cluster': 0, 'attribute': 5, 'data': 'lumi.sensor_cube',
'name': 'type', 'value': 'lumi.sensor_cube', 'type': str},
Expand Down Expand Up @@ -151,7 +150,6 @@ def test_template(self):

device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'})
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.remote.b286acn01'})
self.assertTrue(device.load_template())
self.assertCountEqual(device.attributes,
[{'endpoint': 1, 'cluster': 0, 'attribute': 4, 'data': 'LUMI',
'name': 'manufacturer', 'value': 'LUMI'},
Expand All @@ -174,10 +172,9 @@ def test_template(self):

def test_inverse_bool(self):
device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'}, self.zigate)
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.sensor_switch.aq2'})
device.set_attribute(1, 6, {'attribute': 0, 'lqi': 255, 'data': True})
self.assertTrue(device.get_property_value('onoff'))
device.load_template()
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': 'lumi.sensor_switch.aq2'})
device.set_attribute(1, 6, {'attribute': 0, 'lqi': 255, 'data': True})
self.assertFalse(device.get_property_value('onoff'))

Expand All @@ -187,12 +184,12 @@ def test_templates(self):
for f in files:
success = False
try:
print('Test template', f)
print('Test template', f, f[:-5])
with open(os.path.join(path, f)) as fp:
json.load(fp)
device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'}, self.zigate)
device.set_attribute(1, 0, {'attribute': 5, 'lqi': 255, 'data': f[:-5]})
self.assertTrue(device.load_template())
self.assertEqual(device.discovery, 'templated')
success = True
except Exception as e:
print(e)
Expand Down Expand Up @@ -232,6 +229,20 @@ def test_fast_change(self):
time.sleep(core.DELAY_FASTCHANGE + 1)
self.assertEqual(device.get_property_value('onoff'), False)

def test_quirks(self):
device = core.Device({'addr': '1234', 'ieee': '0123456789abcdef'})
device.set_attribute(1, 0x0000, {'attribute': 0xff01, 'lqi': 255,
'data': '0121130b0421a84305211300062401000000006429ed0965219513662be18201000a210000'})
self.assertEqual(device.get_property_value('xiaomi'), {1: 2835,
4: 17320,
5: 19,
6: b'\x01\x00\x00\x00\x00',
100: 2541,
101: 5013,
102: 99041,
10: 0})
self.assertEqual(device.get_property_value('battery_voltage'), 2.835)


if __name__ == '__main__':
unittest.main()
13 changes: 10 additions & 3 deletions zigate/clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,18 @@ class C0000(Cluster):
0x0007: {'name': 'power_source', 'value': 'value'},
0x0010: {'name': 'description',
'value': 'clean_str(value)'},
0xff01: {'name': 'battery_voltage',
'value': "struct.unpack('H', unhexlify(value)[2:4])[0]/1000.",
'unit': 'V', 'type': float},
# 0xff01: {'name': 'battery_voltage',
# 'value': "struct.unpack('H', unhexlify(value)[2:4])[0]/1000.",
# 'unit': 'V', 'type': float},
0xff01: {'name': 'xiaomi',
'value': "decode_xiaomi(value)",
'type': dict},
0xff02: {'name': 'battery_voltage',
'value': "struct.unpack('H', unhexlify(value)[3:5])[0]/1000.",
'unit': 'V', 'type': float},
# 0xff02: {'name': 'xiaomi',
# 'value': "decode_xiaomi(value)",
# 'type': dict},
}

def update(self, data):
Expand All @@ -190,6 +196,7 @@ def update(self, data):


def decode_xiaomi(rawdata):
rawdata = unhexlify(rawdata)
data = {}
i = 0
while i < len(rawdata):
Expand Down
72 changes: 44 additions & 28 deletions zigate/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,26 +678,9 @@ def interpret_response(self, response):
return
device = self._get_device(response['addr'])
device.lqi = response['lqi']
r = device.set_attribute(response['endpoint'],
response['cluster'],
response.cleaned_data())
if r is None:
return
added, attribute_id = r
changed = device.get_attribute(response['endpoint'],
response['cluster'],
attribute_id, True)
if response['cluster'] == 0 and attribute_id == 5:
if not device.discovery:
device.load_template()
if added:
dispatch_signal(ZIGATE_ATTRIBUTE_ADDED, self, **{'zigate': self,
'device': device,
'attribute': changed})
else:
dispatch_signal(ZIGATE_ATTRIBUTE_UPDATED, self, **{'zigate': self,
'device': device,
'attribute': changed})
device.set_attribute(response['endpoint'],
response['cluster'],
response.cleaned_data())
elif response.msg == 0x004D: # device announce
LOGGER.debug('Device Announce %s', response)
device = Device(response.data, self)
Expand All @@ -708,9 +691,9 @@ def interpret_response(self, response):
if response['addr'] == self.addr:
return
device = self._get_device(response['addr'])
r = device.set_attribute(response['endpoint'],
response['cluster'],
response.cleaned_data())
device.set_attribute(response['endpoint'],
response['cluster'],
response.cleaned_data())
elif response.msg == 0x8501: # OTA image block request
LOGGER.debug('Client is requesting ota image data')
self._ota_send_image_data(response)
Expand Down Expand Up @@ -2241,11 +2224,11 @@ def action_ias_warning(self, addr, endpoint,
addr_mode, addr_fmt = self._choose_addr_mode(addr)
addr = self.__addr(addr)
manufacturer_specific = manufacturer_code != 0
mode = {'stop': '0000', 'burglar': '1000', 'fire': '0100', 'emergency': '1100',
'policepanic': '0010', 'firepanic': '1010', 'emergencypanic': '0110'
mode = {'stop': '0000', 'burglar': '0001', 'fire': '0010', 'emergency': '0011',
'policepanic': '0100', 'firepanic': '0101', 'emergencypanic': '0110'
}.get(mode, '0000')
strobe = '10' if strobe else '00'
level = {'low': '00', 'medium': '10', 'high': '01', 'veryhigh': '11'}.get(level, '00')
strobe = '01' if strobe else '00'
level = {'low': '00', 'medium': '01', 'high': '10', 'veryhigh': '11'}.get(level, '00')
warning_mode_strobe_level = int(mode + strobe + level, 2)
strobe_level = {'low': 0, 'medium': 1, 'high': 2, 'veryhigh': 3}.get(strobe_level, 0)
data = struct.pack('!B' + addr_fmt + 'BBBBHBHBB', addr_mode, addr, 1,
Expand All @@ -2271,7 +2254,7 @@ def action_ias_squawk(self, addr, endpoint,
manufacturer_specific = manufacturer_code != 0
mode = {'armed': '0000', 'disarmed': '1000'}.get(mode, '0000')
strobe = '1' if strobe else '0'
level = {'low': '00', 'medium': '10', 'high': '01', 'veryhigh': '11'}.get(level, '00')
level = {'low': '00', 'medium': '01', 'high': '10', 'veryhigh': '11'}.get(level, '00')
squawk_mode_strobe_level = int(mode + strobe + '0' + level, 2)
data = struct.pack('!B' + addr_fmt + 'BBBBHB', addr_mode, addr, 1,
endpoint,
Expand Down Expand Up @@ -3056,8 +3039,40 @@ def set_attribute(self, endpoint_id, cluster_id, data):
self._lock_release()
if not r:
return
changed = self.get_attribute(endpoint_id,
cluster_id,
attribute['attribute'], True)
if cluster_id == 0 and attribute['attribute'] == 5:
if not self.discovery:
self.load_template()
if added:
dispatch_signal(ZIGATE_ATTRIBUTE_ADDED, self._zigate,
**{'zigate': self._zigate,
'device': self,
'attribute': changed})
else:
dispatch_signal(ZIGATE_ATTRIBUTE_UPDATED, self._zigate,
**{'zigate': self._zigate,
'device': self,
'attribute': changed})

self._handle_quirks(changed)

return added, attribute['attribute']

def _handle_quirks(self, attribute):
"""
Handle special attributes
"""
if 'name' not in attribute:
return
if attribute['name'] == 'xiaomi':
LOGGER.debug('Handle special xiaomi attribute %s', attribute)
values = attribute['value']
# Battery voltage
self.set_attribute(0x0001, 0x0001, {'attribute': 0x0020, 'data': values[1] / 100.})
# TODO: Handle more special attribute

def _delay_change(self, endpoint_id, cluster_id, data):
'''
Delay attribute change
Expand Down Expand Up @@ -3284,6 +3299,7 @@ def load_template(self):
return
path = os.path.join(BASE_PATH, 'templates', template_filename + '.json')
success = False
LOGGER.debug('Try loading template %s', path)
if os.path.exists(path):
try:
with open(path) as fp:
Expand Down
2 changes: 1 addition & 1 deletion zigate/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
#


__version__ = '0.39.0'
__version__ = '0.40.0'

0 comments on commit 30a8a5d

Please sign in to comment.