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) diff --git a/README.md b/README.md index e2a6d8af8..e78529f58 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,19 @@ 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. + +## 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 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_appdb.py b/tests/test_appdb.py index 29439e981..d3b36148b 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) @@ -128,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) diff --git a/tests/test_application.py b/tests/test_application.py index 751037491..1f17d9c3d 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 @@ -195,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/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/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/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/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/__init__.py b/zigpy/__init__.py index 1f1b828a5..025a43df9 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 MAJOR_VERSION = 0 -MINOR_VERSION = 11 +MINOR_VERSION = 12 PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) 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..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 @@ -41,6 +44,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 +94,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): @@ -336,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.""" 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/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) 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 diff --git a/zigpy/ota/provider.py b/zigpy/ota/provider.py index ce02d5f1f..908b7238b 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): @@ -142,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: 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]}, - } - } 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