From 6173dd5a74fdd3860c0c91f5575f67648b601884 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 10 Nov 2019 10:05:02 -0500 Subject: [PATCH 01/14] 0.12.0.dev0 version bump. --- zigpy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zigpy/__init__.py b/zigpy/__init__.py index eb28e3c82..1712306f4 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 MAJOR_VERSION = 0 -MINOR_VERSION = 10 -PATCH_VERSION = "1.dev0" +MINOR_VERSION = 12 +PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 5fab968dcb6d03a62528996a1d5121f1cce04349 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 19 Nov 2019 21:41:14 -0500 Subject: [PATCH 02/14] Extract IKEA OTA image offset and size directly from the container (#259) * Extract the offset and size of the IKEA OTA image from the container directly * Revert accidental commit of changes to unrelated test --- tests/test_ota_provider.py | 13 +++++++++---- zigpy/ota/provider.py | 14 ++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/test_ota_provider.py b/tests/test_ota_provider.py index a02a2cc79..a00749f6e 100644 --- a/tests/test_ota_provider.py +++ b/tests/test_ota_provider.py @@ -324,18 +324,23 @@ async def test_ikea_refresh_list_locked(mock_get, ikea_prov, image_with_version) @pytest.mark.asyncio @patch("aiohttp.ClientSession.get") async def test_ikea_fetch_image(mock_get, image_with_version): - prefix = b"\x00This is extra data\x00\x55\xaa" - data = ( + data = bytes.fromhex( "1ef1ee0b0001380000007c11012178563412020054657374204f544120496d61" "676500000000000000000000000000000000000042000000" ) - data = binascii.unhexlify(data) sub_el = b"\x00\x00\x04\x00\x00\x00abcd" + + container = bytearray(b"\x00This is extra data\x00\x55\xaa" * 100) + container[0:4] = b'NGIS' + container[16:20] = (512).to_bytes(4, 'little') # offset + container[20:24] = len(data + sub_el).to_bytes(4, 'little') # size + container[512:512 + len(data) + len(sub_el)] = data + sub_el + img = image_with_version(image_type=0x2101) img.url = mock.sentinel.url mock_get.return_value.__aenter__.return_value.read = CoroutineMock( - side_effect=[prefix + data + sub_el] + side_effect=[container] ) r = await img.fetch_image() diff --git a/zigpy/ota/provider.py b/zigpy/ota/provider.py index ce02d5f1f..4d5e98d5f 100644 --- a/zigpy/ota/provider.py +++ b/zigpy/ota/provider.py @@ -82,8 +82,6 @@ def expired(self) -> bool: @attr.s class IKEAImage: - OTA_HEADER = 0x0BEEF11E .to_bytes(4, "little") - manufacturer_id = attr.ib() image_type = attr.ib() version = attr.ib(default=None) @@ -108,7 +106,15 @@ async def fetch_image(self) -> Optional[OTAImage]: LOGGER.debug("Downloading %s for %s", self.url, self.key) async with req.get(self.url) as rsp: data = await rsp.read() - offset = data.index(self.OTA_HEADER) + + assert len(data) > 24 + offset = int.from_bytes(data[16:20], 'little') + size = int.from_bytes(data[20:24], 'little') + assert len(data) > offset + size + + ota_image, _ = OTAImage.deserialize(data[offset:offset + size]) + assert ota_image.key == self.key + LOGGER.debug( "Finished downloading %s bytes from %s for %s ver %s", self.image_size, @@ -116,7 +122,7 @@ async def fetch_image(self) -> Optional[OTAImage]: self.key, self.version, ) - return OTAImage.deserialize(data[offset:])[0] + return ota_image class Trådfri(Basic): From 623eb6b8d3ecec3afbbcf7f633d595db2a4a1d13 Mon Sep 17 00:00:00 2001 From: Hedda Date: Wed, 20 Nov 2019 20:29:53 +0100 Subject: [PATCH 03/14] Added a "How to contribute" section to README.md (#261) Links and suggestion of hardware donations. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index e2a6d8af8..3bd20f7ab 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ zigpy works with separate radio libraries which can each interface with multiple - [RaspBee](https://www.dresden-elektronik.de/raspbee/) GPIO radio adapter from [Dresden-Elektronik](https://www.dresden-elektronik.de) ### Experimental Zigbee radio modules + - ZiGate based radios (via the [zigpy-zigate](https://github.com/doudz/zigpy-zigate) library for zigpy) - [ZiGate open source ZigBee adapter hardware](https://zigate.fr/) @@ -42,6 +43,14 @@ Packages of tagged versions are also released via PyPI - https://pypi.org/project/zigpy-deconz/ - https://pypi.org/project/zigpy-zigate/ +## How to contribute + +If you are looking to make a contribution to this project we suggest that you follow the steps in these guides: +- https://github.com/firstcontributions/first-contributions/blob/master/README.md +- https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md + +Some developers might also be interested in receiving donations in the form of hardware such as Zigbee modules or devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirect improve the development of this project. + ## Related projects ### ZHA Device Handlers From 35c7d60a1327b2135faea31a6964027f1c07a9d6 Mon Sep 17 00:00:00 2001 From: Hedda Date: Wed, 20 Nov 2019 20:32:43 +0100 Subject: [PATCH 04/14] Create Contributors.md (#260) Create Contributors.md as per https://github.com/firstcontributions/first-contributions/blob/master/README.md --- Contributors.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Contributors.md diff --git a/Contributors.md b/Contributors.md new file mode 100644 index 000000000..288ef4076 --- /dev/null +++ b/Contributors.md @@ -0,0 +1,22 @@ +# Contributors +- [Russell Cloran] (https://github.com/rcloran) +- [Alexei Chetroi] (https://github.com/Adminiuga) +- [damarco] (https://github.com/damarco) +- [Andreas Bomholtz] (https://github.com/AndreasBomholtz) +- [puddly] (https://github.com/puddly) +- [presslab-us] (https://github.com/presslab-us) +- [Igor Bernstein] (https://github.com/igorbernstein2) +- [David F. Mulcahey] (https://github.com/dmulcahey) +- [Yoda-x] (https://github.com/Yoda-x) +- [Solomon_M] (https://github.com/zalke) +- [Pascal Vizeli] (https://github.com/pvizeli) +- [prairiesnpr] (https://github.com/prairiesnpr) +- [Jurriaan Pruis] (https://github.com/jurriaan) +- [Marcel Hoppe] (https://github.com/hobbypunk90) +- [felixstorm] (https://github.com/felixstorm) +- [Dinko Bajric] (https://github.com/dbajric) +- [Abílio Costa] (https://github.com/abmantis) +- [https://github.com/SchaumburgM] (https://github.com/SchaumburgM) +- [https://github.com/Nemesis24] (https://github.com/Nemesis24) +- [Hedda] (https://github.com/Hedda) +- [Andreas Setterlind] (https://github.com/Gamester17) From 311cc005e289a78a82f8b661853585e677b88e5a Mon Sep 17 00:00:00 2001 From: Hedda Date: Thu, 21 Nov 2019 19:32:10 +0100 Subject: [PATCH 05/14] Add a "Developer references" section to README.md (#262) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 3bd20f7ab..e78529f58 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ If you are looking to make a contribution to this project we suggest that you fo Some developers might also be interested in receiving donations in the form of hardware such as Zigbee modules or devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirect improve the development of this project. +## Developer references + +Silicon Labs video playlist of ZigBee Concepts: Architecture basics, MAC/PHY, node types, and application profiles +- https://www.youtube.com/playlist?list=PL-awFRrdECXvAs1mN2t2xaI0_bQRh2AqD + ## Related projects ### ZHA Device Handlers From 4de6e57b4d04a04dca16e78093160f1a59794045 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 1 Dec 2019 14:02:59 -0500 Subject: [PATCH 06/14] Don't enforce content-type for IKEA fw requests. (#265) --- zigpy/ota/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy/ota/provider.py b/zigpy/ota/provider.py index 4d5e98d5f..908b7238b 100644 --- a/zigpy/ota/provider.py +++ b/zigpy/ota/provider.py @@ -148,7 +148,7 @@ async def refresh_firmware_list(self) -> None: async with self._locks[LOCK_REFRESH]: async with aiohttp.ClientSession() as req: async with req.get(self.UPDATE_URL) as rsp: - fw_lst = await rsp.json(content_type="application/octet-stream") + fw_lst = await rsp.json(content_type=None) self._cache.clear() for fw in fw_lst: if "fw_file_version_MSB" not in fw: From 8ba32384cf6b3a77c6e1cd3a712f294ad1849454 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 1 Dec 2019 14:03:43 -0500 Subject: [PATCH 07/14] Allow inactive endpoints (#266) * allow inactive endpoints * fix constants * add test * fix test * fix comparison... doh * comments --- tests/test_endpoint.py | 12 ++++++++++++ zigpy/endpoint.py | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index adb2588fb..df2ffea62 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -36,6 +36,18 @@ async def mockrequest(nwk, epid, tries=None, delay=None): assert 6 in ep.out_clusters +@pytest.mark.asyncio +async def test_inactive_initialize(ep): + async def mockrequest(nwk, epid, tries=None, delay=None): + sd = types.SimpleDescriptor() + sd.endpoint = 2 + return [131, None, sd] + + ep._device.zdo.Simple_Desc_req = mockrequest + await ep.initialize() + assert ep.status == endpoint.Status.ENDPOINT_INACTIVE + + @pytest.mark.asyncio async def test_initialize_zha(ep): return await _test_initialize(ep, 260) diff --git a/zigpy/endpoint.py b/zigpy/endpoint.py index 69fa96d33..761bdbe56 100644 --- a/zigpy/endpoint.py +++ b/zigpy/endpoint.py @@ -8,6 +8,7 @@ import zigpy.zcl from zigpy.zcl.clusters.general import Basic from zigpy.zcl.foundation import Status as ZCLStatus +from zigpy.zdo.types import Status as zdo_status LOGGER = logging.getLogger(__name__) @@ -19,6 +20,8 @@ class Status(enum.IntEnum): NEW = 0 # Endpoint information (device type, clusters, etc) init done ZDO_INIT = 1 + # Endpoint Inactive + ENDPOINT_INACTIVE = 3 class Endpoint(zigpy.util.LocalLogMixin, zigpy.util.ListenableMixin): @@ -47,7 +50,11 @@ async def initialize(self): sdr = await self._device.zdo.Simple_Desc_req( self._device.nwk, self._endpoint_id, tries=3, delay=2 ) - if sdr[0] != 0: + if sdr[0] == zdo_status.NOT_ACTIVE: + # These endpoints are essentially junk but this lets the device join + self.status = Status.ENDPOINT_INACTIVE + return + elif sdr[0] != zdo_status.SUCCESS: raise Exception("Failed to retrieve service descriptor: %s", sdr) except Exception as exc: self.warn("Failed ZDO request during endpoint initialization: %s", exc) From e3d2b93e2388366f0321b1be3f34575edbb9a7dd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 1 Dec 2019 15:05:06 -0500 Subject: [PATCH 08/14] Fix group removal. (#267) --- tests/test_group.py | 8 ++++---- zigpy/group.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_group.py b/tests/test_group.py index 99f4cdd3a..531c419d2 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -83,7 +83,7 @@ def test_add_group_no_evt(groups, monkeypatch): def test_pop_group_id(groups, endpoint): group = groups[FIXTURE_GRP_ID] group.add_member(endpoint) - group.remove_member = mock.MagicMock() + group.remove_member = mock.MagicMock(side_effect=group.remove_member) groups.listener_event.reset_mock() assert FIXTURE_GRP_ID in groups @@ -91,7 +91,7 @@ def test_pop_group_id(groups, endpoint): assert isinstance(grp, zigpy.group.Group) assert FIXTURE_GRP_ID not in groups - assert groups.listener_event.call_count == 1 + assert groups.listener_event.call_count == 2 assert group.remove_member.call_count == 1 assert group.remove_member.call_args[0][0] is endpoint @@ -103,13 +103,13 @@ def test_pop_group(groups, endpoint): assert FIXTURE_GRP_ID in groups group = groups[FIXTURE_GRP_ID] group.add_member(endpoint) - group.remove_member = mock.MagicMock() + group.remove_member = mock.MagicMock(side_effect=group.remove_member) groups.listener_event.reset_mock() grp = groups.pop(group) assert isinstance(grp, zigpy.group.Group) assert FIXTURE_GRP_ID not in groups - assert groups.listener_event.call_count == 1 + assert groups.listener_event.call_count == 2 assert group.remove_member.call_count == 1 assert group.remove_member.call_args[0][0] is endpoint diff --git a/zigpy/group.py b/zigpy/group.py index 1a0f1016b..54b07680a 100644 --- a/zigpy/group.py +++ b/zigpy/group.py @@ -113,13 +113,13 @@ def pop(self, item, *args) -> Optional[Group]: if isinstance(item, Group): group = super().pop(item.group_id, *args) if isinstance(group, Group): - for member in group.values(): + for member in (*group.values(),): group.remove_member(member) self.listener_event("group_removed", group) return group group = super().pop(item, *args) if isinstance(group, Group): - for member in group.values(): + for member in (*group.values(),): group.remove_member(member) self.listener_event("group_removed", group) return group From 7314966e140f7fe7c8da143087d02372cce93e50 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 14 Dec 2019 18:18:02 -0500 Subject: [PATCH 09/14] Static routing framework. (#268) Device() class to hold "relays" list for static routing. Save relay list updates to database. --- tests/test_appdb.py | 10 ++++++++++ zigpy/appdb.py | 34 ++++++++++++++++++++++++++++++++++ zigpy/application.py | 3 +++ zigpy/device.py | 20 ++++++++++++++++---- zigpy/types/named.py | 6 ++++++ 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/tests/test_appdb.py b/tests/test_appdb.py index 29439e981..c315b0b31 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -53,6 +53,8 @@ async def test_database(tmpdir, monkeypatch): app = make_app(db) # TODO: Leaks a task on dev.initialize, I think? ieee = make_ieee() + relays_1 = [t.NWK(0x1234), t.NWK(0x2345)] + relays_2 = [t.NWK(0x3456), t.NWK(0x4567)] app.handle_join(99, ieee, 0) app.handle_join(99, ieee, 0) @@ -75,6 +77,7 @@ async def test_database(tmpdir, monkeypatch): clus._update_attribute(5, bytes("Model", "ascii")) clus.listener_event("cluster_command", 0) clus.listener_event("general_command") + dev.relays = relays_1 # Test a CustomDevice custom_ieee = make_ieee(1) @@ -89,6 +92,7 @@ async def test_database(tmpdir, monkeypatch): assert isinstance(app.get_device(custom_ieee), CustomDevice) assert ep.endpoint_id in dev.get_signature() app.device_initialized(app.get_device(custom_ieee)) + dev.relays = relays_2 # Everything should've been saved - check that it re-loads with mock.patch("zigpy.quirks.get_device", fake_get_device): @@ -103,7 +107,11 @@ async def test_database(tmpdir, monkeypatch): assert dev.endpoints[2].model == "Model" assert dev.endpoints[2].out_clusters[1].cluster_id == 1 assert dev.endpoints[3].device_type == profiles.zll.DeviceType.COLOR_LIGHT + assert dev.relays == relays_1 + dev = app2.get_device(custom_ieee) + assert dev.relays == relays_2 + dev.relays = None app.handle_leave(99, ieee) @@ -119,6 +127,8 @@ async def mockleave(*args, **kwargs): app3 = make_app(db) assert ieee not in app3.devices + dev = app2.get_device(custom_ieee) + assert dev.relays is None os.unlink(db) diff --git a/zigpy/appdb.py b/zigpy/appdb.py index 23bf281ee..86260c760 100644 --- a/zigpy/appdb.py +++ b/zigpy/appdb.py @@ -43,6 +43,7 @@ def __init__(self, database_file, application): self._create_table_attributes() self._create_table_groups() self._create_table_group_members() + self._create_table_relays() self._application = application @@ -64,6 +65,14 @@ def device_left(self, device): def device_removed(self, device): self._remove_device(device) + def device_relays_updated(self, device, relays): + """Device relay list is updated.""" + if relays is None: + self._save_device_relays_clear(device.ieee) + return + + self._save_device_relays_update(device.ieee, t.Relays(relays).serialize()) + def attribute_updated(self, cluster, attrid, value): self._save_attribute( cluster.endpoint.device.ieee, @@ -161,6 +170,14 @@ def _create_table_group_members(self): "group_members_idx", "group_members", "group_id, ieee, endpoint_id" ) + def _create_table_relays(self): + self._create_table( + "relays", + """(ieee ieee, relays, + FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE)""", + ) + self._create_index("relays_idx", "relays", "ieee") + def _enable_foreign_keys(self): self.execute("PRAGMA foreign_keys = ON") @@ -237,6 +254,15 @@ def _save_attribute(self, ieee, endpoint_id, cluster_id, attrid, value): self.execute(q, (ieee, endpoint_id, cluster_id, attrid, value)) self._db.commit() + def _save_device_relays_update(self, ieee, value): + q = "INSERT OR REPLACE INTO relays VALUES (?, ?)" + self.execute(q, (ieee, value)) + self._db.commit() + + def _save_device_relays_clear(self, ieee): + self.execute("DELETE FROM relays WHERE ieee = ?", (ieee,)) + self._db.commit() + def _scan(self, table, filter=None): if filter is None: return self.execute("SELECT * FROM %s" % (table,)) @@ -282,6 +308,7 @@ def _load_attributes(filter=None): _load_attributes() self._load_groups() self._load_group_members() + self._load_relays() def _load_devices(self): for (ieee, nwk, status) in self._scan("devices"): @@ -329,3 +356,10 @@ def _load_group_members(self): group.add_member( self._application.get_device(ieee).endpoints[ep_id], suppress_event=True ) + + def _load_relays(self): + for (ieee, value) in self._scan("relays"): + dev = self._application.get_device(ieee) + dev.relays = t.Relays.deserialize(value)[0] + for dev in self._application.devices.values(): + dev.add_context_listener(self) diff --git a/zigpy/application.py b/zigpy/application.py index da7c2b044..6c25f9fbe 100644 --- a/zigpy/application.py +++ b/zigpy/application.py @@ -41,6 +41,7 @@ def __init__(self, database_file=None): ota_dir = os.path.join(ota_dir, OTA_DIR) self.ota.initialize(ota_dir) + self._dblistener = None if database_file is not None: self._dblistener = zigpy.appdb.PersistingListener(database_file, self) self.add_listener(self._dblistener) @@ -90,6 +91,8 @@ def device_initialized(self, device): self.listener_event("raw_device_initialized", device) device = zigpy.quirks.get_device(device) self.devices[device.ieee] = device + if self._dblistener is not None: + device.add_context_listener(self._dblistener) self.listener_event("device_initialized", device) async def remove(self, ieee): diff --git a/zigpy/device.py b/zigpy/device.py index 157cdb221..5f2755145 100644 --- a/zigpy/device.py +++ b/zigpy/device.py @@ -3,14 +3,14 @@ import enum import logging import time +from typing import Optional import zigpy.endpoint import zigpy.exceptions +from zigpy.types import NWK, BroadcastAddress, Relays import zigpy.util -import zigpy.zdo as zdo import zigpy.zcl.foundation as foundation -from zigpy.types import BroadcastAddress, NWK - +import zigpy.zdo as zdo APS_REPLY_TIMEOUT = 5 APS_REPLY_TIMEOUT_EXTENDED = 28 @@ -28,7 +28,7 @@ class Status(enum.IntEnum): ENDPOINTS_INIT = 2 -class Device(zigpy.util.LocalLogMixin): +class Device(zigpy.util.LocalLogMixin, zigpy.util.ListenableMixin): """A device on the network""" def __init__(self, application, ieee, nwk): @@ -43,11 +43,13 @@ def __init__(self, application, ieee, nwk): self.last_seen = None self.status = Status.NEW self.initializing = False + self._listeners = {} self._manufacturer = None self._model = None self.node_desc = zdo.types.NodeDescriptor() self._node_handle = None self._pending = zigpy.util.Requests() + self._relays = None def schedule_initialize(self): if self.initializing: @@ -287,6 +289,16 @@ def model(self, value): if isinstance(value, str): self._model = value + @property + def relays(self) -> Optional[Relays]: + """Relay list.""" + return self._relays + + @relays.setter + def relays(self, relays: Optional[Relays]) -> None: + self._relays = relays + self.listener_event("device_relays_updated", relays) + def __getitem__(self, key): return self.endpoints[key] diff --git a/zigpy/types/named.py b/zigpy/types/named.py index dd5ef7c3c..e8aab0f1a 100644 --- a/zigpy/types/named.py +++ b/zigpy/types/named.py @@ -118,3 +118,9 @@ class TimeOfDay(Struct): class UTCTime(basic.uint32_t): pass + + +class Relays(basic.LVList(NWK)): + """Relay list for static routing.""" + + pass From 4164e35d1f64ee76a86627b6e3f43f1f1067f522 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 14 Dec 2019 18:55:56 -0500 Subject: [PATCH 10/14] Fix task leaking in tests. (#269) --- tests/test_appdb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_appdb.py b/tests/test_appdb.py index c315b0b31..d3b36148b 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -138,8 +138,9 @@ def _test_null_padded(tmpdir, test_manufacturer=None, test_model=None): app = make_app(db) # TODO: Leaks a task on dev.initialize, I think? ieee = make_ieee() - app.handle_join(99, ieee, 0) - app.handle_join(99, ieee, 0) + with mock.patch("zigpy.device.Device.schedule_initialize"): + app.handle_join(99, ieee, 0) + app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) From b3e08d0d82a685b39461ca3d8997c4aebf5d6b72 Mon Sep 17 00:00:00 2001 From: prairiesnpr Date: Sat, 21 Dec 2019 16:31:41 -0500 Subject: [PATCH 11/14] Remove quirks from zigpy (#270) * Changed the device type to Dimmable Light * Remove quirks from zigpy * Remove KOF test --- tests/test_quirks.py | 38 ------ zigpy/quirks/__init__.py | 7 -- zigpy/quirks/ikea/__init__.py | 47 ------- zigpy/quirks/keen/__init__.py | 27 ---- zigpy/quirks/kof/__init__.py | 109 ----------------- zigpy/quirks/smartthings/__init__.py | 102 --------------- zigpy/quirks/xiaomi/__init__.py | 177 --------------------------- 7 files changed, 507 deletions(-) delete mode 100644 zigpy/quirks/ikea/__init__.py delete mode 100644 zigpy/quirks/keen/__init__.py delete mode 100644 zigpy/quirks/kof/__init__.py delete mode 100644 zigpy/quirks/smartthings/__init__.py delete mode 100644 zigpy/quirks/xiaomi/__init__.py diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 0a78d99f0..e70c828ca 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -283,44 +283,6 @@ class MyCluster(zigpy.quirks.CustomCluster): assert Device not in zigpy.quirks._DEVICE_REGISTRY -def test_kof_no_reply(): - class TestCluster(zigpy.quirks.kof.NoReplyMixin, zigpy.quirks.CustomCluster): - cluster_id = 0x1234 - void_input_commands = [0x0002] - server_commands = { - 0x0001: ("noop", (), False), - 0x0002: ("noop_noreply", (), False), - } - client_commands = {} - - ep = mock.MagicMock() - cluster = TestCluster(ep) - - cluster.command(0x0001) - ep.request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, expect_reply=True, command_id=mock.ANY - ) - ep.reset_mock() - - cluster.command(0x0001, expect_reply=False) - ep.request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, expect_reply=False, command_id=mock.ANY - ) - ep.reset_mock() - - cluster.command(0x0002) - ep.request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, expect_reply=False, command_id=mock.ANY - ) - ep.reset_mock() - - cluster.command(0x0002, expect_reply=True) - ep.request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, expect_reply=True, command_id=mock.ANY - ) - ep.reset_mock() - - def test_custom_cluster_idx(): class TestClusterIdx(zigpy.quirks.CustomCluster): cluster_Id = 0x1234 diff --git a/zigpy/quirks/__init__.py b/zigpy/quirks/__init__.py index a68125455..0ed0c6455 100644 --- a/zigpy/quirks/__init__.py +++ b/zigpy/quirks/__init__.py @@ -94,10 +94,3 @@ def set_device_attr(attr): class CustomCluster(zigpy.zcl.Cluster): _skip_registry = True - - -from . import xiaomi # noqa: F401, F402 -from . import smartthings # noqa: F401, F402 -from . import kof # noqa: F401, F402 -from . import keen # noqa: F401, F402 -from . import ikea # noqa: F401, F402 diff --git a/zigpy/quirks/ikea/__init__.py b/zigpy/quirks/ikea/__init__.py deleted file mode 100644 index 292a5fa5a..000000000 --- a/zigpy/quirks/ikea/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -from zigpy.quirks import CustomDevice - - -class TradfriPlug(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x010A, - "input_clusters": [0, 3, 4, 5, 6, 8, 64636], - "output_clusters": [5, 25, 32], - }, - # - 2: { - "profile_id": 0xC05E, - "device_type": 0x0010, - "input_clusters": [4096], - "output_clusters": [4096], - }, - # - 242: { - "profile_id": 0xA1E0, - "device_type": 0x0061, - "input_clusters": [33], - "output_clusters": [33], - }, - }, - "manufacturer": "IKEA of Sweden", - } - - replacement = { - "endpoints": { - 1: { - "profile_id": 0x0104, - "device_type": 0x010A, - "input_clusters": [0, 3, 4, 5, 6, 64636], - "output_clusters": [5, 25, 32], - } - } - } diff --git a/zigpy/quirks/keen/__init__.py b/zigpy/quirks/keen/__init__.py deleted file mode 100644 index dded61154..000000000 --- a/zigpy/quirks/keen/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from zigpy.quirks import CustomDevice - - -class KeenTemperatureHumiditySensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x0302, - "input_clusters": [0, 3, 1, 32], - "output_clusters": [0, 4, 3, 5, 25, 1026, 1029, 1027, 32], - } - }, - "manufacturer": "Keen Home Inc", - } - - replacement = { - "endpoints": { - 1: { - "input_clusters": [0x0000, 0x0003, 0x0402, 0x0405], - "output_clusters": [0x0403], - } - } - } diff --git a/zigpy/quirks/kof/__init__.py b/zigpy/quirks/kof/__init__.py deleted file mode 100644 index eb21a3e50..000000000 --- a/zigpy/quirks/kof/__init__.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -This module handles quirks of the King of Fans MR101Z ceiling fan receiver. - -The King of Fans ceiling fan receiver does not generate default replies. This -module overrides all server commands that do not have a mandatory reply to not - expect replies at all. -""" -from zigpy.quirks import CustomDevice, CustomCluster -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - Groups, - Scenes, - OnOff, - LevelControl, - Ota, -) -from zigpy.zcl.clusters.hvac import Fan - - -class NoReplyMixin(object): - """ - A simple mixin that allows a cluster to have configureable list of command - ids that do not generate an explicit reply. - """ - - void_input_commands = [] - - def command(self, command, *args, manufacturer=None, expect_reply=None): - """ - Overrides Cluster#command to configure expect_reply behavior based on - void_input_commands. Note that this method changes the default value of - expect_reply to None. This allows the caller to explicitly force - expect_reply to true. - """ - if expect_reply is None: - expect_reply = command not in self.void_input_commands - - return super(NoReplyMixin, self).command( - command, *args, manufacturer=manufacturer, expect_reply=expect_reply - ) - - -class KofBasic(NoReplyMixin, CustomCluster, Basic): - void_input_commands = [0x00] - - -class KofIdentify(NoReplyMixin, CustomCluster, Identify): - # Identify, Trigger Effect - void_input_commands = [0x00, 0x40] - - -class KofGroups(NoReplyMixin, CustomCluster, Groups): - # Remove All Groups, Add Group If Identifying - void_input_commands = [0x04, 0x05] - - -class KofScenes(NoReplyMixin, CustomCluster, Scenes): - # Recall Scene - void_input_commands = [0x05] - - -class KofOnOff(NoReplyMixin, CustomCluster, OnOff): - # All - void_input_commands = [0x00, 0x01, 0x02, 0x40, 0x41, 0x42] - - -class KofLevelControl(NoReplyMixin, CustomCluster, LevelControl): - # All - void_input_commands = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07] - - -class CeilingFan(CustomDevice): - signature = { - "endpoints": { - 1: { - "profile_id": 0x0104, - "device_type": 14, - "input_clusters": [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Fan.cluster_id, - ], - "output_clusters": [Identify.cluster_id, Ota.cluster_id], - } - }, - "manufacturer": "King Of Fans, Inc.", - } - - replacement = { - "endpoints": { - 1: { - "input_clusters": [ - KofBasic, - KofIdentify, - KofGroups, - KofScenes, - KofOnOff, - KofLevelControl, - Fan, - ], - "output_clusters": [Identify, Ota], - } - } - } diff --git a/zigpy/quirks/smartthings/__init__.py b/zigpy/quirks/smartthings/__init__.py deleted file mode 100644 index 277a2d985..000000000 --- a/zigpy/quirks/smartthings/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -import zigpy.types as t -from zigpy.quirks import CustomDevice, CustomCluster - - -class SmartthingsRelativeHumidityCluster(CustomCluster): - cluster_id = 0xFC45 - name = "Smartthings Relative Humidity Measurement" - ep_attribute = "humidity" - attributes = { - # Relative Humidity Measurement Information - 0x0000: ("measured_value", t.int16s) - } - server_commands = {} - client_commands = {} - - -class SmartthingsTemperatureHumiditySensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x0302, - "input_clusters": [0, 1, 3, 32, 1026, 2821, 64581], - "output_clusters": [3, 25], - } - } - } - - replacement = { - "endpoints": { - 1: { - "input_clusters": [ - 0x0000, - 0x0001, - 0x0003, - 0x0402, - 0x0B05, - SmartthingsRelativeHumidityCluster, - ] - } - } - } - - -class SmartThingsAccelCluster(CustomCluster): - cluster_id = 0xFC02 - name = "Smartthings Accelerometer" - ep_attribute = "accelerometer" - attributes = { - 0x0000: ("motion_threshold_multiplier", t.uint8_t), - 0x0002: ("motion_threshold", t.uint16_t), - 0x0010: ("acceleration", t.bitmap8), # acceleration detected - 0x0012: ("x_axis", t.int16s), - 0x0013: ("y_axis", t.int16s), - 0x0014: ("z_axis", t.int16s), - } - - client_commands = {} - server_commands = {} - - -class SmartthingsMultiPurposeSensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x0402, - "input_clusters": [ - 0, - 1, - 3, - 32, - 1026, - 1280, - SmartThingsAccelCluster.cluster_id, - ], - "output_clusters": [3, 25], - } - } - } - - replacement = { - "endpoints": { - 1: { - "input_clusters": [ - 0x0000, - 0x0001, - 0x0003, - 0x0020, - 0x0402, - 0x0500, - SmartThingsAccelCluster, - ], - "output_clusters": [0x0003, 0x0019], - } - } - } diff --git a/zigpy/quirks/xiaomi/__init__.py b/zigpy/quirks/xiaomi/__init__.py deleted file mode 100644 index 1023fce6e..000000000 --- a/zigpy/quirks/xiaomi/__init__.py +++ /dev/null @@ -1,177 +0,0 @@ -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - Groups, - Scenes, - MultistateInput, - AnalogInput, - Ota, -) - - -class TemperatureHumiditySensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x5F01, - "model": "lumi.sensor_ht", - "manufacturer": "LUMI", - "input_clusters": [0, 3, 25, 65535, 18], - "output_clusters": [0, 4, 3, 5, 25, 65535, 18], - }, - # - 2: { - "profile_id": 0x0104, - "device_type": 0x5F02, - "input_clusters": [3, 18], - "output_clusters": [4, 3, 5, 18], - }, - # - 3: { - "profile_id": 0x0104, - "device_type": 0x5F03, - "input_clusters": [3, 12], - "output_clusters": [4, 3, 5, 12], - }, - }, - "manufacturer": "LUMI", - } - - replacement = { - "endpoints": {1: {"input_clusters": [0x0000, 0x0003, 0x0402, 0x0405]}} - } - - -class AqaraTemperatureHumiditySensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x5F01, - "input_clusters": [0, 3, 65535, 1026, 1027, 1029], - "output_clusters": [0, 4, 65535], - } - }, - "manufacturer": "LUMI", - } - - replacement = { - "endpoints": {1: {"input_clusters": [0x0000, 0x0003, 0x0402, 0x0403, 0x0405]}} - } - - -class AqaraOpenCloseSensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x5F01, - "input_clusters": [0, 3, 65535, 6], - "output_clusters": [0, 4, 65535], - } - }, - "manufacturer": "LUMI", - } - - replacement = { - "endpoints": { - 1: { - "input_clusters": [0x0000, 0x0003], - "output_clusters": [0x0000, 0x0004, 0x0006], - } - } - } - - -class AqaraWaterSensor(CustomDevice): - signature = { - "endpoints": { - # - 1: { - "profile_id": 0x0104, - "device_type": 0x0402, - "input_clusters": [0, 3, 1], - "output_clusters": [25], - } - }, - "manufacturer": "LUMI", - } - - replacement = { - "endpoints": {1: {"input_clusters": [0x0000, 0x0003, 0x0001, 0x0500]}} - } - - -class AqaraMagicCubeSensor(CustomDevice): - signature = { - "endpoints": { - 1: { - "profile_id": 0x0104, - "device_type": 0x5F01, - "input_clusters": [ - Basic.cluster_id, - Identify.cluster_id, - MultistateInput.cluster_id, - Ota.cluster_id, - ], - "output_clusters": [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - MultistateInput.cluster_id, - Ota.cluster_id, - ], - }, - 2: { - "profile_id": 0x0104, - "device_type": 0x5F02, - "input_clusters": [Identify.cluster_id, MultistateInput.cluster_id], - "output_clusters": [ - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - MultistateInput.cluster_id, - ], - }, - 3: { - "profile_id": 0x0104, - "device_type": 0x5F03, - "input_clusters": [Identify.cluster_id, AnalogInput.cluster_id], - "output_clusters": [ - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - AnalogInput.cluster_id, - ], - }, - }, - "manufacturer": "LUMI", - } - - replacement = { - "endpoints": { - 1: {"input_clusters": [Basic.cluster_id, Identify.cluster_id]}, - 2: {"input_clusters": [MultistateInput.cluster_id]}, - 3: {"input_clusters": [AnalogInput.cluster_id]}, - } - } From dd8b766330c757154112cba5609c1bf19eb6cadd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 26 Dec 2019 10:31:42 -0500 Subject: [PATCH 12/14] Tests cleanup (#272) --- tests/test_application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_application.py b/tests/test_application.py index 751037491..d9d5a2aea 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -67,7 +67,6 @@ async def test_permit(app, ieee): await app.permit(node=ncp_ieee) assert app.devices[ieee].zdo.permit.call_count == 1 assert app.permit_ncp.call_count == 1 - print("{}".format(asyncio.Task.all_tasks())) @pytest.mark.asyncio From 87ef3678c39b4db6780c4f0065557467472b4d50 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 26 Dec 2019 10:32:29 -0500 Subject: [PATCH 13/14] Add configuration dict option (#271) --- setup.py | 14 +++----------- tests/test_application.py | 4 ++++ zigpy/application.py | 10 +++++++++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index ed50006b8..41e3de27d 100644 --- a/setup.py +++ b/setup.py @@ -11,15 +11,7 @@ author="Russell Cloran", author_email="rcloran@gmail.com", license="GPL-3.0", - packages=find_packages(exclude=['*.tests']), - install_requires=[ - 'aiohttp', - 'pycryptodome', - 'crccheck', - ], - tests_require=[ - 'asynctest', - 'pytest', - 'pytest-asyncio', - ], + packages=find_packages(exclude=["*.tests"]), + install_requires=["aiohttp", "crccheck", "pycryptodome", "voluptuous"], + tests_require=["asynctest", "pytest", "pytest-asyncio"], ) diff --git a/tests/test_application.py b/tests/test_application.py index d9d5a2aea..1f17d9c3d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -194,6 +194,10 @@ def test_nwk(app): assert app.nwk == app._nwk +def test_config(app): + assert app.config == app._config + + def test_deserialize(app, ieee): dev = mock.MagicMock() app.deserialize(dev, 1, 1, b"") diff --git a/zigpy/application.py b/zigpy/application.py index 6c25f9fbe..237179ab5 100644 --- a/zigpy/application.py +++ b/zigpy/application.py @@ -3,6 +3,7 @@ import os.path from typing import Optional +import voluptuous as vol import zigpy.appdb import zigpy.device import zigpy.group @@ -14,17 +15,19 @@ import zigpy.zdo import zigpy.zdo.types as zdo_types +CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) DEFAULT_ENDPOINT_ID = 1 LOGGER = logging.getLogger(__name__) OTA_DIR = "zigpy_ota/" class ControllerApplication(zigpy.util.ListenableMixin): - def __init__(self, database_file=None): + def __init__(self, database_file=None, config={}): self._send_sequence = 0 self.devices = {} self._groups = zigpy.group.Groups(self) self._listeners = {} + self._config = CONFIG_SCHEMA(config) self._channel = None self._channels = None self._ext_pan_id = None @@ -339,6 +342,11 @@ def channels(self): """Channel mask.""" return self._channels + @property + def config(self) -> dict: + """Return current configuration.""" + return self._config + @property def extended_pan_id(self): """Extended PAN Id.""" From 9f76d81f8331dc1cab8a8f48a92b97960fae87f2 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 26 Dec 2019 13:08:31 -0500 Subject: [PATCH 14/14] Version bump 0.12.0 --- zigpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy/__init__.py b/zigpy/__init__.py index 1712306f4..025a43df9 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 MAJOR_VERSION = 0 MINOR_VERSION = 12 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION)