diff --git a/.agents/skills/dev-workflow/SKILL.md b/.agents/skills/dev-workflow/SKILL.md new file mode 100644 index 0000000000..23f4ce17df --- /dev/null +++ b/.agents/skills/dev-workflow/SKILL.md @@ -0,0 +1,264 @@ +--- +name: dev-workflow +description: Setting up the development environment, deploying Edge Drivers to hubs, and sharing drivers with other users via channels and invites +--- + +# SmartThings Edge Driver Development Workflow + +This skill covers environment setup, driver deployment to hubs, and sharing +drivers with other users through channels and invite links. + +--- + +## Environment Setup + +### 1. Install Lua 5.3 + +Edge Drivers are Lua-based. Install the Lua 5.3 runtime for local development +and linting: + +```bash +# Ubuntu / Debian +sudo apt install lua5.3 + +# macOS +brew install lua@5.3 + +# Windows +# Download the Lua 5.3 binary from https://luabinaries.sourceforge.net/download.html +# Or install via scoop: +scoop install lua +# Or via chocolatey: +choco install lua53 +``` + +### 2. lua_libs Directory + +The `lua_libs/` directory contains the SmartThings Lua libraries that are +available on the hub at runtime. These correspond to the assets attached to the +latest release on GitHub: + + + +Download the lua_libs archive from the release assets and +extract it into the repository root if it is missing or needs updating. + +### 3. Configure LUA_PATH + +Set `LUA_PATH` so that `require` resolves both your driver modules and the +SmartThings library modules in `lua_libs/`: + +```bash +export LUA_PATH="./?.lua;./?/init.lua;$(pwd)/lua_libs/?.lua;$(pwd)/lua_libs/?/init.lua;;" +``` + +Run it from the repository root so `$(pwd)` resolves correctly. + + +### 4. Install the SmartThings CLI + +The CLI is required for packaging, deploying, and managing drivers and +channels on the platform. + +```bash +# Via npm (requires Node.js >= 24.8.0) +npm install -g @smartthings/cli + +# macOS via Homebrew +brew install smartthingscommunity/smartthings/smartthings + +# Linux / Windows +# Download the binary or installer from: +# https://github.com/SmartThingsCommunity/smartthings-cli/releases +``` + +Verify the installation: + +```bash +smartthings --version +``` + +The CLI uses browser-based OAuth login by default. Run `smartthings devices` to trigger +the login flow. + +### 5. Python Requirements (Testing) + +Some test and tooling scripts require Python dependencies: + +```bash +pip install -r tools/requirements.txt +``` + +### 6. Install Luacheck (Linting) + +Luacheck provides static analysis for Lua source files. It requires LuaRocks +(the Lua package manager). + +**Install LuaRocks first:** + +```bash +# Ubuntu / Debian +sudo apt install luarocks + +# macOS +brew install luarocks + +# Windows +# Download the installer from https://luarocks.org/releases/ +# Or via chocolatey: +choco install luarocks +``` + +**Then install Luacheck:** + +```bash +# Via LuaRocks (all platforms) +luarocks install luacheck + +# macOS alternative (installs both luarocks and luacheck) +brew install luacheck +``` + +Run it against a driver directory: + +```bash +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/ +``` + +--- + +## Deploying Drivers + +### Overview + +Deploying a driver to a physical hub requires three things: + +1. A **channel** you own. +2. The hub **enrolled** in that channel. +3. The driver **packaged and uploaded** through the CLI. + +### Step 1: Create a Channel + +```bash +smartthings edge:channels:create +``` + +You will be prompted for a name and description. Note the returned channel ID. + +### Step 2: Enroll Your Hub + +```bash +smartthings edge:channels:enroll +``` + +Select the channel when prompted, or pass `--channel `. + +Find your hub ID with: + +```bash +smartthings devices --type HUB +``` + +### Step 3: Package and Install the Driver + +The `edge:drivers:package` command can build, upload, assign to a channel, and +install in one step: + +```bash +smartthings edge:drivers:package \ + --hub= \ + --channel= +``` + +For example: + +```bash +smartthings edge:drivers:package drivers/SmartThings/zwave-switch \ + --hub=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee \ + --channel=11111111-2222-3333-4444-555555555555 +``` + +### Other Useful Deployment Commands + +```bash +# List drivers installed on a hub +smartthings edge:drivers:installed --hub= + +# Stream logs from a driver on the hub +smartthings edge:drivers:logcat --hub= + +# Uninstall a driver from a hub +smartthings edge:drivers:uninstall --hub= + +# Remove unused drivers from a hub +smartthings edge:drivers:prune --hub= + +# Switch a device to a different driver +smartthings edge:drivers:switch +``` + +--- + +## Sharing Drivers + +### Creating an Invite Link + +Invite links let other users install your driver from your channel without +giving them ownership of the driver or channel. + +```bash +smartthings edge:channels:invites:create +``` + +You will be prompted to select a channel and a driver. The command returns an +invite URL of the form: + +``` +https://bestow-regional.api.smartthings.com/invite/ +``` + +Share this URL with users. They open it in a browser or the SmartThings mobile +app to accept the invitation. + +### Enrollment Flow for Recipients + +1. The recipient opens the invite link. +2. They log in to their Samsung / SmartThings account. +3. They select a hub to enroll in the channel. +4. The driver can be selected to install to that hub. + +### Managing Invites + +```bash +# List existing invites +smartthings edge:channels:invites + +# Delete an invite +smartthings edge:channels:invites:delete +``` + +### Managing Channel Assignments + +```bash +# Assign a specific driver version to a channel +smartthings edge:channels:assign + +# List drivers assigned to a channel +smartthings edge:channels:drivers + +# Remove a driver from a channel +smartthings edge:channels:unassign +``` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Create channel | `smartthings edge:channels:create` | +| Enroll hub | `smartthings edge:channels:enroll ` | +| Package & deploy | `smartthings edge:drivers:package --hub= --channel=` | +| Stream logs | `smartthings edge:drivers:logcat --hub=` | +| Create invite | `smartthings edge:channels:invites:create` | +| List installed drivers | `smartthings edge:drivers:installed --hub=` | diff --git a/.agents/skills/linting-and-style/SKILL.md b/.agents/skills/linting-and-style/SKILL.md new file mode 100644 index 0000000000..83578854ce --- /dev/null +++ b/.agents/skills/linting-and-style/SKILL.md @@ -0,0 +1,96 @@ +--- +name: linting-and-style +description: Running luacheck for Lua linting and following code style conventions in Edge Driver development +--- + +# Linting and Code Style for Edge Drivers + +## Running Luacheck + +```bash +luacheck --config .github/workflows/.luacheckrc +``` + +### Examples + +```bash +# Lint a specific driver +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/ + +# Lint a single file +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/src/init.lua + +# Lint the entire repo +luacheck --config .github/workflows/.luacheckrc . +``` + +Luacheck runs automatically in CI on pull requests that modify files under `drivers/` (see `.github/workflows/luacheck.yml`). + +## Code Style Conventions + +These conventions are observed across the Edge Driver codebase: + +### General + +- **Indentation**: 2 spaces, no tabs +- **Strings**: Use double quotes `"string"` for module requires and general strings +- **Local variables**: Always use `local` for variables and functions at module scope +- **Line length**: No enforced limit, but most code stays under 120 characters + +### Naming + +- **Variables and functions**: `snake_case` (e.g., `local mock_device`, `local function test_init()`) +- **Constants**: `UPPER_SNAKE_CASE` for true constants (e.g., `SENSOR_BINARY`) +- **Modules**: Return a table at the end of the file (`return module_name`) + +### Requires and Imports + +```lua +-- Standard library requires first +local capabilities = require "st.capabilities" +local zw = require "st.zwave" + +-- Then test/integration requires +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +-- Then protocol-specific requires +local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) +``` + +### Function Style + +- Prefer `local function name()` over `local name = function()` +- Handler functions typically receive `(driver, device, ...)` arguments +- Use early returns for guard clauses + +### Tables + +- Trailing commas are common and acceptable in multi-line tables +- Align table entries for readability in test manifests + +### Comments + +- Use `--` for single-line comments +- Minimal inline comments; code should be self-documenting + +Copyright header at the top of every file: +```lua +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +``` + +### File Organization for Drivers + +``` +driver-name/ + src/ + init.lua -- Main driver entry point + .lua -- Additional driver modules + test/ + test_*.lua -- Test files (must start with test_) + profiles/ + *.yml -- Device profiles + fingerprints.yml -- Device fingerprints + config.yml -- Driver configuration +``` diff --git a/.agents/skills/testing-edge-drivers/SKILL.md b/.agents/skills/testing-edge-drivers/SKILL.md new file mode 100644 index 0000000000..a34a22c901 --- /dev/null +++ b/.agents/skills/testing-edge-drivers/SKILL.md @@ -0,0 +1,349 @@ +--- +name: testing-edge-drivers +description: Running and writing integration tests for SmartThings Edge Drivers using the Python test harness and Lua integration test framework +--- + +# Testing SmartThings Edge Drivers + +## Running Tests + +Tests are run via the Python test harness: + +```bash +python3 tools/run_driver_tests.py [options] +``` + +### Options + +| Flag | Description | +|------|-------------| +| `-v` | Print individual test names and pass/fail status | +| `-vv` | Print test names, status, and full logs on failures (recommended) | +| `-vvv` | Print all logs from all tests | +| `-f ` | Only run tests whose file path matches the regex filter | +| `-j ` | Output JUnit XML results to the specified file | +| `-c [files]` | Run with luacov code coverage | +| `--html` | Generate HTML coverage reports (use with `-c`) | + +### Filter Examples + +```bash +# Run all tests for a specific driver +python3 tools/run_driver_tests.py -vv -f "zwave-smoke-alarm" + +# Run a specific test file +python3 tools/run_driver_tests.py -vv -f "test_zwave_smoke_detector" + +# Run all zigbee switch tests +python3 tools/run_driver_tests.py -vv -f "zigbee-switch" + +# Run all virtual device tests +python3 tools/run_driver_tests.py -vv -f "virtual" +``` + +The filter is a regex applied to the full file path. The harness searches for files matching `drivers/*/*/src/test/test_*.lua`. + +### Python Requirements + +Install dependencies before running tests: + +```bash +pip install -r tools/requirements.txt +``` + +Required packages: `junit_xml`, `requests`, `PyYAML`, `regex`. + +### How Tests Execute + +The Python harness (`tools/run_driver_tests.py`): +1. Globs for all `test_*.lua` files under `drivers/*/src/test/` +2. Filters by the `-f` regex if provided +3. Changes directory to the driver's `src/` directory (two levels up from the test file) +4. Runs each test file with `lua ` +5. Parses stdout for `Running test`, `PASSED`, `FAILED`, and summary lines +6. Reports totals and exits with code 1 if any tests failed + +## Integration Test Framework + +The framework lives in `lua_libs/integration_test/` and is required as `integration_test` in test files. It provides: + +### Core Modules + +| Module | Purpose | +|--------|---------| +| `integration_test` (init.lua) | Main test runner, registration, mock device builder | +| `integration_test.utils` | Utility functions like `get_profile_definition()` | +| `integration_test.mock_device` | Build mock Zigbee, Z-Wave, Matter, or generic devices | +| `integration_test.zwave_test_utils` | Z-Wave specific helpers (e.g., `zwave_test_build_receive_command`) | +| `integration_test.zigbee_test_utils` | Zigbee specific helpers | +| `integration_test.mock_socket` | Mock socket layer with channel-based message routing | + +### Channels + +The test framework uses channels to simulate communication between the driver and the platform: + +- `zwave` - Z-Wave protocol messages +- `zigbee` - Zigbee protocol messages +- `matter` - Matter protocol messages +- `capability` - SmartThings capability events (commands from cloud, events to cloud) +- `device_lifecycle` - Device lifecycle events (init, added, removed, etc.) +- `driver_lifecycle` - Driver lifecycle events +- `timer` - Timer-related events + +Each channel supports two directions: +- `receive` - Messages sent TO the driver (incoming commands, device reports) +- `send` - Messages sent FROM the driver (capability events, protocol commands) + +## Writing Tests + +### Test File Structure + +Every test file follows this pattern: + +```lua +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +-- 1. Build mock device(s) +local mock_device = test.mock_device.build_test_generic_device({ + profile = t_utils.get_profile_definition("my-profile.yml"), +}) + +-- 2. Define test init function (runs before each test) +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +-- 3. Register tests (message tests or coroutine tests) + +-- 4. Run all registered tests +test.run_registered_tests() +``` + +### Building Mock Devices + +```lua +-- Generic device (no protocol) +local mock = test.mock_device.build_test_generic_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + preferences = { ["certifiedpreferences.somePref"] = true }, +}) + +-- Z-Wave device +local zw = require "st.zwave" +local mock = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + zwave_endpoints = { + { + command_classes = { + { value = zw.SENSOR_BINARY }, + { value = zw.NOTIFICATION }, + } + } + } +}) + +-- Zigbee device +local mock = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + zigbee_endpoints = { ... } +}) +``` + +### Message Tests (`register_message_test`) + +Message tests define an ordered sequence of receive/send message pairs. Each receive triggers the driver handler, and the subsequent sends are the expected outputs. + +```lua +test.register_message_test( + "Test description", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + }, + { + min_api_version = 17 -- optional version constraint + } +) +``` + +The manifest is an array of message entries. The framework groups them into blocks: each block starts with a `receive` followed by zero or more `send` entries. The receives are queued on the mock channel; the sends are set as expectations. The driver processes the receive and the framework asserts the expected sends occurred. + +### Coroutine Tests (`register_coroutine_test`) + +For more complex test logic (multiple interactions, state changes, conditional assertions, timer manipulation): + +```lua +test.register_coroutine_test( + "Test with complex logic", + function() + -- Queue a lifecycle event + test.socket.device_lifecycle():__queue_receive({ mock_device.id, "init" }) + test.socket.device_lifecycle():__queue_receive( + mock_device:generate_info_changed({ + preferences = { ["certifiedpreferences.somePref"] = false } + }) + ) + test.wait_for_events() + + -- Now send a capability command and expect a response + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.on()) + ) + end, + { + min_api_version = 17 + } +) +``` + +Key coroutine test APIs: +- `test.socket.:__queue_receive(msg)` - Queue a message for the driver to receive +- `test.socket.:__expect_send(msg)` - Set an expectation for a message the driver should send +- `test.wait_for_events()` - Yield to let the driver process queued messages and check expectations +- `test.mock_time.advance_time(seconds)` - Advance the mock clock + +### Real Example: Z-Wave Smoke Detector Test + +From `drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua`: + +```lua +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" + +local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) + +local sensor_endpoints = { + { + command_classes = { + { value = zw.SENSOR_BINARY }, + { value = zw.SENSOR_ALARM }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("smoke-battery-temperature-tamperalert-temperaturealarm.yml"), + zwave_endpoints = sensor_endpoints +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Sensor Binary report (smoke) should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + zw_test_utils.zwave_test_build_receive_command( + SensorBinary:Report({ + sensor_type = SensorBinary.sensor_type.SMOKE, + sensor_value = SensorBinary.sensor_value.DETECTED_AN_EVENT + }) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.smokeDetector.smoke.detected()) + } + }, + { min_api_version = 17 } +) + +test.run_registered_tests() +``` + +## Common Test Patterns + +### Testing Capability Commands (cloud -> device) + +Receive on `capability` channel, expect protocol message on `zwave`/`zigbee`/`matter`: + +```lua +{ + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } } +}, +{ + channel = "zwave", + direction = "send", + message = ... -- expected Z-Wave command +} +``` + +### Testing Device Reports (device -> cloud) + +Receive on protocol channel, expect capability event on `capability`: + +```lua +{ + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(...) } +}, +{ + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) +} +``` + +### Testing Lifecycle Events + +```lua +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "added" }) +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "init" }) +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "doConfigure" }) +``` + +### Testing Preference Changes + +```lua +test.socket.device_lifecycle():__queue_receive( + mock_device:generate_info_changed({ + preferences = { ["certifiedpreferences.myPref"] = new_value } + }) +) +``` + +### Optional Test Parameters + +The `opts` table passed to `register_message_test` or `register_coroutine_test` supports: + +| Field | Description | +|-------|-------------| +| `min_api_version` | Skip test if API version is below this (commonly set to 17) | +| `max_api_version` | Skip test if API version is above this | +| `test_init` | Per-test init function (overrides the global `set_test_init_function`) | +| `expected_error` | String or array of Lua patterns for expected errors | +| `inner_block_ordering` | Set to `"relaxed"` to allow sends in any order within a block | diff --git a/.agents/skills/understanding-lua-libraries/SKILL.md b/.agents/skills/understanding-lua-libraries/SKILL.md new file mode 100644 index 0000000000..235f637b69 --- /dev/null +++ b/.agents/skills/understanding-lua-libraries/SKILL.md @@ -0,0 +1,295 @@ +--- +name: understanding-lua-libraries +description: Understanding the SmartThings Edge Driver Lua libraries - driver lifecycle, message dispatchers, default handlers, and protocol message objects +--- + +# SmartThings Edge Driver Lua Library Architecture + +## 1. Driver Initialization and Run Loop + +A driver is created by calling `Driver("name", template)` (or a protocol-specific variant like `ZigbeeDriver("name", template)`). The template is a Lua table containing handler tables and configuration. + +The base `Driver.init` (in `lua_libs/st/driver.lua`) does the following: +- Sets `out_driver.NAME` from the name argument +- Initializes handler tables: `capability_handlers`, `lifecycle_handlers`, `message_handlers` +- Opens communication channels via cosock sockets: `capability_channel`, `environment_channel`, `lifecycle_channel`, `driver_lifecycle_channel`, and optionally `discovery_channel` +- Initializes a datastore and device cache tables +- Calls `Driver.standardize_sub_drivers()` to normalize the `sub_drivers` list +- Builds the `lifecycle_dispatcher` and `capability_dispatcher` from handlers + sub_drivers +- Registers channel handlers so inbound messages get routed to the correct handler function + +The `driver:run()` call starts the cosock event loop, which runs forever processing messages from all registered channels. + +## 2. Message Dispatchers + +The dispatcher system (`lua_libs/st/dispatcher.lua`) is a hierarchical message routing tree. The base class `MessageDispatcher` provides: + +- **`default_handlers`** - handlers at this level of the hierarchy. +- **`child_dispatchers`** - sub-dispatchers (from sub_drivers) that may override defaults +- **`can_handle(driver, device, ...)`** - returns true if this dispatcher or a child can handle the message +- **`dispatch(driver, device, ...)`** - finds and executes the matching handler + +### Dispatch logic + +1. The dispatcher calls `can_handle` on each child dispatcher +2. If any children can handle: **only the children handle it** (parent defaults are NOT called) +3. If multiple children match: ALL matching children receive the message +4. If NO children match: parent defaults are used +5. This is recursive -- sub-drivers can have sub-drivers + +### Dispatcher types + +| Dispatcher | Class | Handles | +|------------|-------|---------| +| `capability_dispatcher` | `CapabilityCommandDispatcher` | Capability commands from the platform (on, off, setLevel, etc.) | +| `lifecycle_dispatcher` | `DeviceLifecycleDispatcher` | Device lifecycle events (added, init, removed, etc.) | +| `zigbee_message_dispatcher` | `ZigbeeMessageDispatcher` | Incoming Zigbee messages (attribute reports, cluster commands, ZDO) | +| `zwave_dispatcher` | `ZwaveDispatcher` | Incoming Z-Wave commands | +| `matter_dispatcher` | `MatterMessageDispatcher` | Incoming Matter interaction responses | +| `secret_data_dispatcher` | `SecretDataDispatcher` | Security/secret data events | + +Each protocol-specific driver (ZigbeeDriver, ZwaveDriver, MatterDriver) adds its own dispatcher on top of the base Driver's capability and lifecycle dispatchers. + +**Zigbee handler structure:** +```lua +zigbee_handlers = { + attr = { -- attribute reports / read responses + [ClusterID] = { + [AttributeID] = handler_function, + } + }, + global = { -- global ZCL commands + [ClusterID] = { + [CommandID] = handler_function, + } + }, + cluster = { -- cluster-specific commands + [ClusterID] = { + [CommandID] = handler_function, + } + }, + zdo = { -- ZDO commands + [ClusterID] = handler_function, + } +} +``` + +**Z-Wave handler structure:** +```lua +zwave_handlers = { + [cc.SWITCH_BINARY] = { -- command class + [SwitchBinary.REPORT] = handler_function, -- command ID + }, +} +``` + +**Matter handler structure:** +```lua +matter_handlers = { + attr = { + [ClusterID] = { + [AttributeID] = handler_function, + } + }, + cmd_response = { ... }, + event = { ... }, + fallback = handler_function, +} +``` + +**Capability handler structure:** +```lua +capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_on, + [capabilities.switch.commands.off.NAME] = handle_off, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, + }, +} +``` + +## 3. Sub-Drivers Pattern + +Sub-drivers allow device-specific behavior overrides gated by a `can_handle` function. A sub-driver is a table with: +- `NAME` (string) +- `can_handle(opts, driver, device, ...) -> boolean` +- Protocol handlers (zigbee_handlers, zwave_handlers, matter_handlers) +- `capability_handlers`, `lifecycle_handlers` +- Optional nested `sub_drivers` + +In practice, sub-drivers are often organized as separate files under `src/sub_drivers/` for clarity, and required in the main driver template. + + +### Dispatch Logic + +1. The dispatcher calls `can_handle` on each child dispatcher +2. If any children can handle: **only the children handle it** (parent defaults are NOT called) +3. If multiple children match: ALL matching children receive the message +4. If NO children match: parent defaults are used +5. This is recursive -- sub-drivers can have sub-drivers + +### Lazy Loading + +Sub-drivers support lazy loading for memory optimization: +- `Driver.lazy_load_sub_driver(sub_driver)`: Strips handlers, keeps only `can_handle` and `NAME` +- `Driver.lazy_load_sub_driver_v2(require_path)`: Even more efficient; only requires `can_handle` and `sub_drivers` modules separately +- A sub-driver with no handlers defined is automatically treated as lazy-loadable + +New sub-drivers must be: +1. Listed in the parent's `sub_drivers.lua` (or the equivalent sub_drivers table) +2. Have a `can_handle.lua` that correctly identifies the target devices +3. Have an `init.lua` that returns the sub-driver table + +If any of these are missing, the sub-driver will not be loaded. + + +## 4. Lifecycle Events + +Device lifecycle events are dispatched through the `DeviceLifecycleDispatcher`. The key events: + +1. **`init`** -- Called for every device on driver startup (existing devices) and after `added` for new devices. Used for setting up component/endpoint mappings and device fields. +2. **`added`** -- Called only when a device is first paired. NOT called for existing devices when a driver is updated. After `added`, a synthetic `init` is automatically dispatched. +3. **`doConfigure`** -- Called when the device needs configuration (typically after pairing). +4. **`infoChanged`** -- Called when device metadata changes (e.g., preferences updated). Receives `args.old_st_store` for comparison. +5. **`removed`** -- Called when device is removed. +6. **`driverSwitched`** -- Called when device switches to this driver. + +Register lifecycle handlers in the template: +```lua +lifecycle_handlers = { + init = device_init, + added = device_added, + removed = device_removed, + doConfigure = device_do_configure, + infoChanged = info_changed_handler, +} +``` + +Handler signature: `function(driver, device, event, args)` + +**Default behaviors provided by the framework:** +- `driverSwitched`: Base Driver marks device as `NONFUNCTIONAL`. ZigbeeDriver overrides this to check capability matching and marks as `PROVISIONED` if all capabilities match. +- `doConfigure`: ZigbeeDriver defaults to `device_management.configure` which sends attribute reporting configuration. +- `added`: After a successful `added` callback, the framework automatically queues a synthetic `init` event. +- `doConfigure`: After success, the framework transitions the device to `PROVISIONED` state. +- Unhandled lifecycle events log a trace message and are otherwise ignored (fallback handler). + +**Critical timing knowledge for lifecycle events** + +1. **`init` on driver startup**. After hub restart the radio may not be ready and sending Zigbee/Z-Wave commands in `init` can fail. +2. **`added` is NOT called for existing devices** on driver update. Only called on first pair. Code that must run for existing devices should go in `init` (for non-radio operations) or use `driverSwitched`. +3. **`doConfigure` is called any time a device is added with the TYPED provisioning state** and is the right place for device-specific configuration commands. +4. **`infoChanged` receives `args.old_st_store`** for comparing old vs new preferences. Drivers should check if a preference actually changed before acting on it. + +## 5. Key Imports and Require Paths + +```lua +-- Base driver (for virtual/LAN devices) +local Driver = require "st.driver" + +-- Protocol-specific drivers +local ZigbeeDriver = require "st.zigbee" +local ZwaveDriver = require "st.zwave.driver" +local MatterDriver = require "st.matter.driver" + +-- Capabilities +local capabilities = require "st.capabilities" + +-- Zigbee defaults (pre-built handlers for common capabilities) +local defaults = require "st.zigbee.defaults" + +-- Zigbee clusters (for building commands/reading attributes) +local zcl_clusters = require "st.zigbee.zcl" + +-- Z-Wave command classes +local cc = require "st.zwave.CommandClass" +local SwitchBinary = require "st.zwave.CommandClass.SwitchBinary" + +-- Matter clusters +local clusters = require "st.matter.clusters" + +-- Utilities +local utils = require "st.utils" +local json = require "st.json" +local log = require "log" + +-- Coroutine runtime +local cosock = require "cosock" + +-- LAN utils +local socket = cosock.socket +local luncheon = require "luncheon" +local luxure = require "luxure" +local lustre = require "lustre" +``` + +### Zigbee driver example (from zigbee-switch) + +```lua +local capabilities = require "st.capabilities" +local ZigbeeDriver = require "st.zigbee" +local defaults = require "st.zigbee.defaults" + +local template = { + supported_capabilities = { + capabilities.switch, + capabilities.switchLevel, + capabilities.colorControl, + capabilities.colorTemperature, + }, + sub_drivers = require("sub_drivers"), + lifecycle_handlers = { + init = device_init, + added = device_added, + }, +} + +-- Register default Zigbee handlers for all supported capabilities +defaults.register_for_default_handlers(template, + template.supported_capabilities, + {native_capability_cmds_enabled = true, native_capability_attrs_enabled = true} +) + +local driver = ZigbeeDriver("zigbee_switch", template) +driver:run() +``` + +This pattern - declare supported capabilities, register defaults, add overrides via sub_drivers and lifecycle_handlers, then construct and run - is the standard structure +for all protocol-based Edge drivers. + +## 6. Default Handlers and Protocol-Specific Default Functionality + +When a driver declares `supported_capabilities` in its template, the framework automatically registers default handlers for each capability. The registration uses `or`-merge +logic: **driver-defined handlers always take precedence over defaults.** If the driver already registered a handler for a given cluster/attribute/command slot, the default +is silently skipped. + +Registration happens in `st.{zigbee,zwave,matter}.defaults.init.lua` via `register_for_default_handlers(driver, capabilities, opts)`: +1. Iterates `supported_capabilities` +2. For each capability, requires the corresponding defaults module +3. Merges `zigbee_handlers`, `zwave_handlers`, or `matter_handlers` (only where driver hasn't defined one) +4. Also merges `attribute_configurations` (Zigbee), `get_refresh_commands` (Z-Wave), or `subscribed_attributes` (Matter) + +### Zigbee specific default functionality + +The default `doConfigure` handler (`device_management.configure`): +1. Sends a `refresh` command (reads all configured attributes) +2. Calls `device:configure()` which iterates all configured attributes and for each: + - Sends a ZDO Bind Request + - Sends a Configure Reporting command with the attribute's min/max interval, data type, and reportable change +3. Also handles IAS Zone enrollment if the device supports cluster `0x0500` + +### Z-Wave specific default functionality + +doConfigure calls `device:default_configure()` which calls `device:refresh()`. The default refresh iterates `get_refresh_commands` from all default capability modules and sends Get commands for each supported CC. +Refresh collects `get_refresh_commands` from all default modules, sends Get commands + +### Matter specific default functionality + +TODO + +## 7. Unit Test Framework + +Load the `testing-edge-drivers` skill for details on the built in unit test framework for to test Zigbee, Z-Wave, and Matter drivers. + diff --git a/.agents/skills/understanding-profiles/SKILL.md b/.agents/skills/understanding-profiles/SKILL.md new file mode 100644 index 0000000000..0f169057e5 --- /dev/null +++ b/.agents/skills/understanding-profiles/SKILL.md @@ -0,0 +1,369 @@ +--- +name: understanding-profiles +description: Understanding and defining SmartThings capabilities, device profiles, preferences, and embedded device configurations for Edge Drivers +--- + +# SmartThings Capabilities, Profiles, and Preferences + +## 1. What Are Capabilities? + +Capabilities are the fundamental abstraction in SmartThings. They define what a device can do and what state it can report. Each capability consists of: + +- **Attributes**: State/status values (e.g., `switch` has attribute `switch` with values `on`/`off`) +- **Commands**: Actions that control the device (e.g., `on()`, `off()`, `setLevel(level)`) + +A capability definition specifies data types, units, and constraints for its attributes and commands. + +### Data Types +| Type | Example | Description | +|------|---------|-------------| +| string | `"locked"` | May have enum or pattern constraints | +| integer | `5` | Whole number, may have min/max | +| number | `5.5` | Fractional values allowed | +| boolean | `true` | true or false | +| object | `{x: 12}` | Map of name-value pairs | +| array | `["heat","cool"]` | List of single type | + +### Common Capabilities +- `switch` - on/off control +- `switchLevel` - dimming (0-100) +- `temperatureMeasurement` - temperature reading +- `battery` - battery percentage +- `contactSensor` - open/closed +- `motionSensor` - active/inactive +- `lock` - locked/unlocked +- `thermostatMode`, `thermostatHeatingSetpoint`, `thermostatCoolingSetpoint` +- `colorTemperature`, `colorControl` +- `refresh` - request device state update +- `firmwareUpdate` - OTA firmware management +- `healthCheck` - device connectivity monitoring + +Full reference: https://developer.smartthings.com/docs/devices/capabilities/capabilities-reference + +## 2. Standard vs Custom Capabilities + +### Standard Capabilities +Standard capabilities live under the `smartthings` namespace but are referenced without a namespace prefix: +```yaml +- id: switch + version: 1 +- id: temperatureMeasurement + version: 1 +``` + +### Custom Capabilities +Custom capabilities use the format `namespace.capabilityName`: +```yaml +- id: perfectlife6617.customGarageDoor + version: 1 +``` + +A namespace is auto-generated per developer account (e.g., `perfectlife6617`). Custom capabilities are created via the SmartThings CLI: +``` +smartthings capabilities:create -i capability.json +``` + +Custom capabilities require a Capability Presentation to render properly in the app. + +## 3. Device Profile YAML Format + +Device profiles define which capabilities a device exposes, organized into components. They live in `profiles/` directories within driver packages. + +### Basic Profile Example (from `zwave-lock`) +```yaml +name: base-lock +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockCodes + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock +``` + +### Multi-Component Profile (from `zigbee-fan`) +```yaml +name: fan-light +components: + - id: main + label: Fan + capabilities: + - id: switch + version: 1 + - id: fanSpeed + version: 1 + config: + values: + - key: "fanSpeed.value" + range: [0, 3] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan + - id: light + label: Light + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + - id: refresh + version: 1 + categories: + - name: Light +``` + +### Profile with Embedded Config and Preferences (from `zigbee-contact`) +```yaml +name: multi-sensor +components: +- id: main + capabilities: + - id: contactSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: threeAxis + version: 1 + - id: accelerationSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MultiFunctionalSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: certifiedpreferences.garageSensor + explicit: true +``` + +### Key Profile Rules +- Must have at least one component; the primary is always `id: main` +- Use multiple components when the same capability is needed more than once (e.g., multi-gang switch) +- Each component needs at least one capability +- `categories` determines the device icon in the app (e.g., `SmartLock`, `Fan`, `Light`, `Thermostat`, `MultiFunctionalSensor`) +- `version: 1` is always used (only version supported) + +## 4. Embedded Device Configurations + +Embedded device configs let you customize the SmartThings app UI directly in the profile YAML, without creating a separate Device Presentation. Only supported by Edge Drivers. + +### Range Constraint +```yaml +- id: colorTemperature + config: + values: + - key: "colorTemperature.value" + range: [2600, 6200] +``` + +### Enabled Values (filter enum options) +```yaml +- id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - heating + - cooling + - fan only + - idle +``` + +### Separate Attribute vs Command Values +```yaml +- id: thermostatMode + config: + values: + - key: thermostatMode.value + enabledValues: + - off + - heat + - eco + - key: setThermostatMode + enabledValues: + - off + - heat +``` + +### Enum Commands +```yaml +- id: alarm + config: + values: + - key: alarm.value + enabledValues: + - off + - siren + - key: "{{enumCommands}}" + enabledValues: + - off + - siren +``` + +When you package the driver, the platform auto-generates a Device Presentation from these configs. + +## 5. Preferences + +Preferences let users configure device behavior from Settings in the SmartThings app. + +### Two Types + +**Explicit (shared/reusable):** Defined externally, referenced by ID in the profile: +```yaml +preferences: + - preferenceId: tempOffset + explicit: true +``` + +Standard explicit preferences include: `tempOffset`, `humidityOffset`, `motionSensitivity`, `reportingInterval`, `reverse`, `presetPosition`, `username`, `password`. + +`tempOffset` and `humidityOffset` are automatically applied by the platform to attribute values - no driver code needed. + +**Embedded (inline in profile):** Defined directly in the profile YAML: +```yaml +preferences: + - title: "IP Address" + name: ipAddress + description: "IP address of the Pi-Hole" + required: true + preferenceType: string + definition: + minLength: 7 + maxLength: 15 + stringType: text + default: localhost +``` + +### Preference Types +| Type | Definition Fields | +|------|------------------| +| boolean | `default` | +| integer | `minimum`, `maximum`, `default` | +| number | `minimum`, `maximum`, `default` | +| string | `stringType` (text/paragraph/password), `minLength`, `maxLength`, `default` | +| enumeration | `options` (key-value map), `default` (must match a key) | + +### Accessing Preferences in Lua + +Query current value: +```lua +local offset = device.preferences.tempOffset +local level = command.args.level + device.preferences.levelOffset +``` + +Handle preference changes via `infoChanged` lifecycle: +```lua +local function device_info_changed(driver, device, event, args) + if args.old_st_store.preferences.sensitivityLevel ~= device.preferences.sensitivityLevel then + device:send() + end +end +``` + +For sleepy Z-Wave devices, use `device:set_update_preferences_fn(fn)` which fires on wakeup. + +## 6. config.yml + +The `config.yml` file is the driver package manifest. It lives at the root of each driver directory. + +```yaml +name: 'Zigbee Thermostat' +defaultProfile: 'thermostat-battery-powerSource' +packageKey: 'zigbee-thermostat' +permissions: + zigbee: {} +description: "SmartThings driver for Zigbee thermostat devices" +vendorSupportInformation: "https://support.smartthings.com" +``` + +### Fields +| Field | Description | +|-------|-------------| +| `name` | Human-readable driver name | +| `packageKey` | Unique package identifier | +| `permissions` | Protocol access: `zigbee: {}`, `zwave: {}`, `lan: {}`, `matter: {}` | +| `description` | Driver description | +| `defaultProfile` | Profile name used when no fingerprint match specifies one | +| `vendorSupportInformation` | Support URL | + +## 7. Fingerprints + +Fingerprints map physical devices to profiles. They live in `fingerprints.yml` at the driver root. + +### Zigbee Fingerprints +```yaml +zigbeeManufacturer: + - id: "LUMI/lumi.motion.ac02" + deviceLabel: Aqara Motion Sensor P1 + manufacturer: LUMI + model: lumi.motion.ac02 + deviceProfileName: motion-illuminance-battery-aqara + - id: "SmartThings/motionv5" + deviceLabel: Motion Sensor + manufacturer: SmartThings + model: motionv5 + deviceProfileName: motion-temp-battery + +zigbeeGeneric: + - id: kickstarter/motion/1 + deviceLabel: SmartThings Motion Sensor + zigbeeProfiles: + - 0xFC01 + deviceIdentifiers: + - 0x013A + deviceProfileName: smartsense-motion +``` + +### Key Fingerprint Fields +| Field | Description | +|-------|-------------| +| `id` | Unique identifier for the fingerprint | +| `deviceLabel` | Default label shown to users | +| `manufacturer` | Device manufacturer string | +| `model` | Device model string | +| `deviceProfileName` | Which profile from `profiles/` to use | +| `zigbeeProfiles` | (zigbeeGeneric) Zigbee profile IDs | +| `deviceIdentifiers` | (zigbeeGeneric) Zigbee device type IDs | + +Z-Wave fingerprints use `manufacturerId`, `productType`, and `productId` instead. + +## 8. Relationship: config.yml + Profiles + Fingerprints + +``` +driver/ +├── config.yml # Package manifest, declares defaultProfile +├── fingerprints.yml # Maps hardware → profile by deviceProfileName +├── profiles/ +│ ├── basic-device.yml # Profile A +│ └── advanced-device.yml # Profile B +└── src/ + └── init.lua # Driver logic +``` + +Flow: +1. A device joins the hub +2. The platform matches it against `fingerprints.yml` entries +3. The matched fingerprint's `deviceProfileName` selects which profile to use +4. If no fingerprint matches, `defaultProfile` from `config.yml` is used +5. The profile defines capabilities, components, categories, and preferences +6. Embedded `config` in the profile customizes the app UI +7. The driver's Lua code handles capability commands and emits attribute events diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..cf17ce46b3 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,52 @@ + You are working in the SmartThings Edge Drivers repository. Drivers are written in **Lua 5.3** and + run on SmartThings hubs. They translate Zigbee, Z-Wave, Matter, and LAN protocol messages into + SmartThings capability commands and events. + + For full context, read `AGENTS.md` at the repository root. It covers driver structure, lifecycle, + profiles, and available skills for deeper domain knowledge. + + ## Standard Commands + + ```bash + # Run tests + python3 tools/run_driver_tests.py -vv -f + + # Lint + luacheck --config .github/workflows/.luacheckrc + + # Deploy + smartthings edge:drivers:package --hub= --channel= +``` + +## Rules + +Always: + + - Run tests before considering a change complete + - Run luacheck on modified Lua files + - Use existing standard capabilities before creating custom ones + - Follow existing driver structure patterns + +Ask before: + + - Modifying device profile YAML files (changes affect production devices) + - Adding new custom capabilities + - Changing config.yml permissions + +Never: + + - Commit hardcoded API keys or tokens + - Skip tests for driver changes + - Use Lua features beyond 5.3 + +## Skills + +Load these files for deeper knowledge when working in each area: + +| Task | Skill file | +|------|-----------| +| Driver lifecycle, dispatch, default handlers | .agents/skills/understanding-lua-libraries/SKILL.md | +| Profiles, capabilities, preferences, fingerprints | .agents/skills/understanding-profiles/SKILL.md | +| Writing and running tests | .agents/skills/testing-edge-drivers/SKILL.md | +| Luacheck / code style | .agents/skills/linting-and-style/SKILL.md | +| Environment setup, deploying, sharing via channels | .agents/skills/dev-workflow/SKILL.md | diff --git a/.github/scripts/check_duplicates.py b/.github/scripts/check_duplicates.py index b4bf01649d..65e2ae2ea6 100644 --- a/.github/scripts/check_duplicates.py +++ b/.github/scripts/check_duplicates.py @@ -5,6 +5,7 @@ cwd = os.getcwd() duplicate_pairs = [] +deleted_profiles = [] def compare_component_capabilities_unordered(comp1, comp2): for cap1 in comp1["capabilities"]: @@ -112,9 +113,16 @@ def compare_components(prof1, prof2): print('\nNEW PROFILE:\n%s is a profile! Comparing to other profiles...' % file) os.chdir(file_directory) - for current_profile in os.listdir("./"): - new_profile = file_basename + new_profile = file_basename + + # Skip deleted files and track them for warning + if not os.path.exists(new_profile): + print("Skipping %s - file was deleted" % new_profile) + deleted_profiles.append(file) + os.chdir(cwd) + continue + for current_profile in os.listdir("./"): # compare to YAML files that are not the same file # Compare only .yml files and only files that have not already been found to be a duplicate if current_profile != new_profile and Path(current_profile).suffix == ".yml" and (current_profile, new_profile) not in duplicate_pairs: @@ -145,7 +153,12 @@ def compare_components(prof1, prof2): for duplicate in duplicate_pairs: f.write("%s == %s\n" % (duplicate[0], duplicate [1])) else: - f.write("Duplicate profile check: Passed - no duplicate profiles detected.") + f.write("Duplicate profile check: Passed - no duplicate profiles detected.\n") + + if deleted_profiles: + f.write("\n:warning: **Deleted profile files detected:**\n") + for deleted in deleted_profiles: + f.write("- `%s`\n" % deleted) with open("profile-comment-body.md", "r") as f: print("\n" + f.read()) diff --git a/.github/workflows/jenkins-driver-tests.yml b/.github/workflows/jenkins-driver-tests.yml index 1e29ea8afc..b9fc07f091 100644 --- a/.github/workflows/jenkins-driver-tests.yml +++ b/.github/workflows/jenkins-driver-tests.yml @@ -1,18 +1,15 @@ name: Run Jenkins driver tests on: - pull_request: + pull_request_target: paths: - 'drivers/**' -permissions: - statuses: write - jobs: trigger-driver-test: strategy: matrix: version: - [ 59, 60 ] + [ 59, 60, dev] runs-on: ubuntu-latest steps: @@ -22,14 +19,15 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | - core.setOutput('status_url', (await github.rest.repos.createCommitStatus({ + let response = await github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, - sha: context.sha, + sha: context.payload.pull_request.head.sha, state: 'pending', description: 'Jenkins job triggered...', context: 'Driver Tests (${{ matrix.version }})' - })).data.url); + }); + core.setOutput('status_url', response.data.url); - name: Trigger Jenkins Generic Webhook env: JENKINS_WEBHOOK_TOKEN: ${{ secrets.JENKINS_WEBHOOK_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ecb9478f81 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# SmartThings Edge Drivers — Agent Instructions + +You are an expert Lua 5.3 engineer and SmartThings Edge Driver maintainer. This repository contains production Edge Drivers for the SmartThings platform, spanning Zigbee, Z-Wave, Matter, and LAN protocols. + +Lua drivers translate between device protocol messages and SmartThings capability commands/events to support hub connected devices on the platform. + +## Repository Structure + +``` +drivers/ # Edge Drivers organized by vendor (SmartThings/, Aqara/, etc.) + // + config.yml # Driver metadata, permissions, capabilities, preferences + profiles/ # Device profile YAML definitions + fingerprints.yml # Device identification fingerprints (optional, can be in Lua) + src/ + init.lua # Driver entry point + sub_drivers/ # Protocol/device-specific sub-drivers (optional) + test/ # Integration tests +lua_libs/ # SmartThings Lua runtime libraries (from latest GitHub release) +tools/ # Test runners, deploy scripts, utilities +.github/workflows/ # CI: tests, luacheck, packaging +``` + +## Standard Commands + +### Run Tests +```bash +python3 tools/run_driver_tests.py -vv -f +``` +The filter matches against driver directory/file names. Load the `testing-edge-drivers` skill for details. + +### Lint +```bash +luacheck --config .github/workflows/.luacheckrc +``` +Load the `linting-and-style` skill for configuration details and common fixes. + +### Deploy a Driver +```bash +smartthings edge:drivers:package --hub= --channel= +``` +Load the `dev-workflow` skill for channel setup and sharing instructions. + +## Driver Anatomy + +Drivers live under `drivers///`. The canonical layout is: + +``` +drivers/// + config.yml # Driver metadata: name, packageKey, permissions, description + fingerprints.yml # Device matching rules (Zigbee, Z-Wave, Matter only) + search-parameters.yml # SSDP/mDNS discovery hints (LAN drivers only) + profiles/ + .yml # One file per device profile + src/ + init.lua # Driver entry point; creates template and calls :run() + sub_drivers.lua # Optional: list of sub-driver require paths + / + init.lua # Sub-driver table: NAME, can_handle, handlers + can_handle.lua # Optional: separated device-matching function + fingerprints.lua # Optional: Lua-side fingerprint list for can_handle +``` + +Load the `understanding-lua-libraries` skill for detailed information on the driver framework. + +### Fingerprints (`fingerprints.yml`) + +Fingerprints tell the platform which driver to assign to a newly-joined device. +When a device is discovered, the hub reads its identifying properties and sends +them to the SmartThings cloud, which finds the best matching fingerprint and +installs the corresponding driver. + +Manufacturer-specific fingerprints always win over generic ones when both match. + +LAN drivers do **not** use `fingerprints.yml`. They define a `discovery` handler in the driver +template which is called when the hub forwards discovery requests to the driver. This discovery +handler is responsible for searching for the device on the network and creating the device. + +### Device Profiles (`profiles/*.yml`) + +A profile declares the SmartThings **capabilities** a device exposes, grouped into +**components**. The `main` component is the primary one. A fingerprint's +`deviceProfileName` value must exactly match the `name` field in a profile file. + +Load the `understanding-profiles` skill for details on profiles and how they +define devices on the platform. + +## Lua Libraries (`lua_libs/`) + +The `lua_libs/` directory at the repository root is setup by the developer and not committed +to the repository. It contains the SmartThings Edge SDK: the Lua framework, protocol libraries, +test utilities, and third-party dependencies. **This directory must be present for tests to run.** +Load the `dev-workflow` skill to help with initial setup. + +Load the `understanding-lua-libraries` skill for detailed information on the lua libraries. + +--- + +## Rules + +### ✅ ALWAYS +- Run tests before considering a change complete +- Run luacheck on modified Lua files +- Use existing capabilities from the SmartThings reference before creating custom ones +- Follow the existing driver structure patterns in this repo +- Use `require` paths relative to `src/` for driver code, and `lua_libs/` for library code + +### ⚠️ ASK FIRST +- Before modifying device profile YAML files (changes affect production devices) +- Before adding new custom capabilities +- Before changing `config.yml` permissions +- Before modifying shared library code in `lua_libs/` + +### 🚫 NEVER +- Commit hardcoded API keys, tokens, or hub UUIDs +- Modify files in `lua_libs/` (these come from upstream releases) +- Skip tests for driver changes +- Use Lua features beyond 5.3 (the hub runtime is Lua 5.3) + +## Available Skills + +Load these for deeper domain knowledge: + +| Skill | When to Use | Skill file | +|-------|-------------|------------| +| `understanding-profiles` | Defining or modifying capabilities, profiles, preferences, or device configurations | .agents/skills/understanding-profiles/SKILL.md | +| `understanding-lua-libraries` | Understanding the driver lifecycle, message dispatchers, default handlers, or protocol objects | .agents/skills/understanding-lua-libraries/SKILL.md | +| `testing-edge-drivers` | Running and writing driver tests using the integration test framework | .agents/skills/testing-edge-drivers/SKILL.md | +| `linting-and-style` | Running luacheck or fixing style issues | .agents/skills/linting-and-style/SKILL.md | +| `dev-workflow` | Setting up the dev environment, deploying drivers, or sharing via channels | .agents/skills/dev-workflow/SKILL.md | diff --git a/drivers/Aqara/aqara-bath-heater/profiles/aqara-bath-heater.yml b/drivers/Aqara/aqara-bath-heater/profiles/aqara-bath-heater.yml index d130be5947..2acda35491 100644 --- a/drivers/Aqara/aqara-bath-heater/profiles/aqara-bath-heater.yml +++ b/drivers/Aqara/aqara-bath-heater/profiles/aqara-bath-heater.yml @@ -8,165 +8,18 @@ components: version: 1 - id: colorTemperature version: 1 - config: - values: - - key: "colorTemperature.value" - range: [2700, 6500] - id: thermostatMode version: 1 - config: - values: - - key: "thermostatMode.value" - enabledValues: - - "off" - - "heat" - - "dryair" - - "cool" - - "fanonly" - id: thermostatHeatingSetpoint version: 1 - config: - values: - - key: "thermostatHeatingSetpoint.value" - range: [16, 45] - unit: "C" - id: fanOscillationMode version: 1 - config: - values: - - key: "fanOscillationMode.value" - enabledValues: - - "swing" - - "fixed" - id: fanMode version: 1 - config: - values: - - key: "fanMode.value" - enabledValues: - - "low" - - "medium" - - "high" - id: refresh version: 1 categories: - name: Thermostat -deviceConfig: - dashboard: - states: - - component: main - capability: switch - version: 1 - - component: main - capability: fanMode - version: 1 - actions: - - component: main - capability: switch - version: 1 - detailView: - - component: main - capability: switch - version: 1 - - component: main - capability: switchLevel - version: 1 - - component: main - capability: colorTemperature - version: 1 - - component: main - capability: thermostatMode - version: 1 - - component: main - capability: thermostatHeatingSetpoint - version: 1 - visibleCondition: - component: main - capability: thermostatMode - version: 1 - value: thermostatMode.value - operator: EQUALS - operand: "heat" - - component: main - capability: fanOscillationMode - version: 1 - visibleCondition: - component: main - capability: thermostatMode - version: 1 - value: thermostatMode.value - operator: ONE_OF - operand: '["heat", "dryair", "cool"]' - - component: main - capability: fanMode - version: 1 - visibleCondition: - component: main - capability: thermostatMode - version: 1 - value: thermostatMode.value - operator: ONE_OF - operand: '["heat", "dryair", "cool", "fanonly"]' - - component: main - capability: refresh - version: 1 - automation: - conditions: - - component: main - capability: switch - version: 1 - - component: main - capability: switchLevel - version: 1 - - component: main - capability: colorTemperature - version: 1 - - component: main - capability: thermostatMode - version: 1 - - component: main - capability: thermostatHeatingSetpoint - version: 1 - - component: main - capability: fanOscillationMode - version: 1 - - component: main - capability: fanMode - version: 1 - values: - - key: "fanMode.value" - enabledValues: - - "low" - - "medium" - - "high" - actions: - - component: main - capability: switch - version: 1 - - component: main - capability: switchLevel - version: 1 - - component: main - capability: colorTemperature - version: 1 - - component: main - capability: thermostatMode - version: 1 - - component: main - capability: thermostatHeatingSetpoint - version: 1 - - component: main - capability: fanOscillationMode - version: 1 - - component: main - capability: fanMode - version: 1 - values: - - key: "setFanMode.fanMode" - enabledValues: - - "low" - - "medium" - - "high" preferences: - preferenceId: stse.nightLightMode explicit: true @@ -178,3 +31,6 @@ preferences: explicit: true - preferenceId: stse.thermostatCtrl explicit: true +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-Aqara_Bath_Heater diff --git a/drivers/Aqara/aqara-bath-heater/src/init.lua b/drivers/Aqara/aqara-bath-heater/src/init.lua index f543a704a8..62007b537c 100644 --- a/drivers/Aqara/aqara-bath-heater/src/init.lua +++ b/drivers/Aqara/aqara-bath-heater/src/init.lua @@ -429,6 +429,7 @@ local function device_added(driver, device) capabilities.fanOscillationMode.fanOscillationMode.NAME) == nil then device:emit_event(capabilities.fanOscillationMode.fanOscillationMode(OSC.SWING)) end + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) end local function send_night_light(device, new) diff --git a/drivers/Aqara/aqara-bath-heater/src/test/test_aqara_bath_heater.lua b/drivers/Aqara/aqara-bath-heater/src/test/test_aqara_bath_heater.lua index 17e85b716b..8dfaa66974 100644 --- a/drivers/Aqara/aqara-bath-heater/src/test/test_aqara_bath_heater.lua +++ b/drivers/Aqara/aqara-bath-heater/src/test/test_aqara_bath_heater.lua @@ -619,6 +619,8 @@ test.register_coroutine_test( capabilities.fanMode.fanMode("medium"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanOscillationMode.fanOscillationMode("swing"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} }))) end ) diff --git a/drivers/SmartThings/matter-appliance/src/init.lua b/drivers/SmartThings/matter-appliance/src/init.lua index 044da51e7c..8f91ebcd0c 100644 --- a/drivers/SmartThings/matter-appliance/src/init.lua +++ b/drivers/SmartThings/matter-appliance/src/init.lua @@ -297,6 +297,7 @@ local matter_driver_template = { capabilities.windMode }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-appliance", matter_driver_template) diff --git a/drivers/SmartThings/matter-energy/src/init.lua b/drivers/SmartThings/matter-energy/src/init.lua index 51d361753a..c6e44008d6 100644 --- a/drivers/SmartThings/matter-energy/src/init.lua +++ b/drivers/SmartThings/matter-energy/src/init.lua @@ -750,6 +750,7 @@ matter_driver_template = { capabilities.battery, capabilities.chargingState }, + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-energy", matter_driver_template) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index b3403863ec..09f384ee4e 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -83,7 +83,13 @@ local function lock_state_handler(driver, device, ib, response) } if ib.data.value ~= nil then - device:emit_event(LOCK_STATE[ib.data.value]) + local event = LOCK_STATE[ib.data.value] + if event ~= nil then + device:emit_event(event) + else + device.log.warn(string.format("Received unknown lock state value (%s), emitting unknown", ib.data.value)) + device:emit_event(attr.unknown()) + end else device:emit_event(LOCK_STATE[LockState.NOT_FULLY_LOCKED]) end @@ -714,6 +720,7 @@ local matter_lock_driver = { doConfigure = do_configure, infoChanged = info_changed, }, + shared_device_thread_enabled = true, } ----------------------------------------------------------------------------------------------------------------------------- diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index fcea79d7f5..ed804363b6 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -46,7 +46,6 @@ local lock_utils = { MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED", ALIRO_READER_CONFIG_UPDATED = "aliroReaderConfigUpdated", LATEST_DOOR_LOCK_FEATURE_MAP = "latestDoorLockFeatureMap", - IS_MODULAR_PROFILE = "isModularProfile" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua index 038aa25d51..375b8c6189 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -7,6 +7,10 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2807}, -- AQARA, U200 Lite {0x115f, 0x2804}, -- AQARA, U400 {0x115f, 0x286A}, -- AQARA, U200 US + {0x115f, 0x2805}, -- Aqara Smart Lock J200 Set + {0x115f, 0x280e}, -- AQARA Smart Gate Lock U500 + {0x115f, 0x280f}, -- AQARA Smart Rim Lock U500 + {0x115f, 0x2810}, -- AQARA Smart Glass Door Lock U500 {0x147F, 0x0001}, -- U-tec {0x147F, 0x0007}, -- ULTRALOQ Bolt Pro Smart Matter Door Lock {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index adcbc04283..3850d71380 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -400,6 +400,7 @@ end local function driver_switched(driver, device) match_profile(driver, device, false) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end -- Matter Handler @@ -1670,6 +1671,7 @@ local function handle_update_credential(driver, device, command) device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + device:set_field(lock_utils.USER_TYPE, nil, {persist = true}) -- Send command local ep = device:component_to_endpoint(command.component) @@ -1727,7 +1729,7 @@ local function set_pin_response_handler(driver, device, ib, response) -- If User Type is Guest and device support schedule, add default schedule local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) - if userType == "guest" and (#week_schedule_eps > 0 or #year_schedule_eps > 0) then + if userType == "guest" and (#week_schedule_eps > 0 or #year_schedule_eps > 0) and cmdName ~= "updateCredential" then local cmdName = "defaultSchedule" local scheduleIdx = 1 diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua index 2477e2745b..9693289951 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -191,6 +191,29 @@ test.register_message_test( } ) +test.register_message_test( + "Handle unknown LockState value from Matter device.", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.DoorLock.attributes.LockState:build_test_report_data( + mock_device, 10, 0xFF + ), + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()), + }, + }, + { + min_api_version = 17 + } +) + test.register_message_test( "Handle received BatPercentRemaining from device.", { { diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index defeafe8e9..492a3c2805 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -82,6 +82,22 @@ end test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-schedule" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() @@ -1650,15 +1666,106 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Handle Update Credential command received from SmartThings.", + "Update Credential for Guest user should not add default schedule again", function() + -- Add Guest user with credential. This sets the USER_TYPE field to "guest". + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockCredentials.ID, + command = "addCredential", + args = {0, "guest", "pin", "654123"} + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + {credential_type = DoorLock.types.CredentialTypeEnum.PIN, credential_index = 1} + ), -- credential + "654123", -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.SUCCESS, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex = 1, userType = "guest"}}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + {{credentialIndex=1, credentialType="pin", userIndex=1}}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule( + mock_device, 1, -- endpoint + 1, -- year_day_index + 1, -- user_index + 0, -- local_start_time + 0xffffffff -- local_end_time + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.SUCCESS -- status + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + {commandName="addCredential", credentialIndex=1, statusCode="success", userIndex=1}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + test.wait_for_events() + -- Update the Guest user's credential. The stale "guest" USER_TYPE field must + -- be cleared so the default schedule is not added again on the response. test.socket.capability:__queue_receive( { mock_device.id, { capability = capabilities.lockCredentials.ID, command = "updateCredential", - args = {1, 1, "pin", "654123"} + args = {1, 1, "pin", "111213"} }, } ) @@ -1671,7 +1778,7 @@ test.register_coroutine_test( DoorLock.types.CredentialStruct( {credential_type = DoorLock.types.CredentialTypeEnum.PIN, credential_index = 1} ), -- credential - "654123", -- credential_data + "111213", -- credential_data 1, -- user_index nil, -- user_status nil -- user_type @@ -1690,6 +1797,7 @@ test.register_coroutine_test( ), } ) + -- The commandResult must be emitted without sending SetYearDaySchedule test.socket.capability:__expect_send( mock_device:generate_test_message( "main", diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua index 04d051b93e..166fa7110a 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua @@ -1,682 +1,682 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local clusters = require "st.matter.clusters" -local cluster_base = require "st.matter.cluster_base" -local DoorLock = clusters.DoorLock -local OctetString1 = require "st.matter.data_types.OctetString1" - -local enabled_optional_component_capability_pairs = {{ - "main", - { - capabilities.lockUsers.ID, - capabilities.lockSchedules.ID, - capabilities.lockAliro.ID - } -}} -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition( - "lock-modular.yml", - {enabled_optional_capabilities = enabled_optional_component_capability_pairs} - ), - manufacturer_info = { - vendor_id = 0x135D, - product_id = 0x00C1, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - { - cluster_id = DoorLock.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0x2510, -- WDSCH & YDSCH & USR & ALIRO - } - }, - device_types = { - { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock - } - } - } -}) - -local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} -local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderVerificationKey:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderGroupIdentifier:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderGroupSubIdentifier:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroGroupResolvingKey:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroBLEAdvertisingVersion:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfAliroEndpointKeysSupported:subscribe(mock_device)) - subscribe_request:merge(cluster_base.subscribe(mock_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) - subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Handle received AliroReaderVerificationKey from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroReaderVerificationKey:build_test_report_data( - mock_device, 1, - "\x04\xA9\xCB\xE4\x18\xEB\x09\x66\x16\x43\xE2\xA4\xA8\x46\xB8\xED\xFE\x27\x86\x98\x30\x2E\x9F\xB4\x3E\x9B\xFF\xD3\xE3\x10\xCC\x2C\x2C\x7F\xF4\x02\xE0\x6E\x40\xEA\x3C\xE1\x29\x43\x52\x73\x36\x68\x3F\xC5\xB1\xCB\x0C\x6A\x7C\x3F\x0B\x5A\xFF\x78\x35\xDF\x21\xC6\x24" - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.readerVerificationKey( - "04a9cbe418eb09661643e2a4a846b8edfe278698302e9fb43e9bffd3e310cc2c2c7ff402e06e40ea3ce12943527336683fc5b1cb0c6a7c3f0b5aff7835df21c624", - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroReaderGroupIdentifier from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroReaderGroupIdentifier:build_test_report_data( - mock_device, 1, - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.readerGroupIdentifier( - "e24f1b205ba923b32cd13dc009e993a8", - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroExpeditedTransactionSupportedProtocolVersions from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:build_test_report_data( - mock_device, 1, - {OctetString1("\x00\x09"), OctetString1("\x01\x00")} - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.expeditedTransactionProtocolVersions( - {"0.9", "1.0"}, - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroSupportedBLEUWBProtocolVersions from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:build_test_report_data( - mock_device, 1, - {OctetString1("\x00\x09"), OctetString1("\x01\x00")} - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.bleUWBProtocolVersions( - {"0.9", "1.0"}, - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroReaderVerificationKey from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:build_test_report_data( - mock_device, 1, - 35 - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.maxCredentialIssuerKeys( - 35, - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroGroupResolvingKey from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroGroupResolvingKey:build_test_report_data( - mock_device, 1, - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.groupResolvingKey( - "e24f1b205ba923b32cd13dc009e993a8", - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received AliroBLEAdvertisingVersion from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.AliroBLEAdvertisingVersion:build_test_report_data( - mock_device, 1, - 1 - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.bleAdvertisingVersion( - "1", - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle received NumberOfAliroEndpointKeysSupported from Matter device.", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.attributes.NumberOfAliroEndpointKeysSupported:build_test_report_data( - mock_device, 1, - 10 - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.maxEndpointKeys( - 10, - {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle Set Card Id command received from SmartThings.", - function() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "setCardId", - args = {"3icub18c8pr00"} - }, - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.cardId("3icub18c8pr00", {visibility = {displayed = false}}) - ) - ) - end -) - -test.register_coroutine_test( - "Handle Set Reader Config command received from SmartThings.", - function() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "setReaderConfig", - args = { - "1a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac", - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "e24f1b205ba923b32cd13dc009e993a8", - nil - } - }, - } - ) - test.socket.matter:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetAliroReaderConfig( - mock_device, 1, -- endpoint - "\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC", - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8", - nil - ), - } - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.server.commands.SetAliroReaderConfig:build_test_command_response( - mock_device, 1, - DoorLock.types.DlStatus.SUCCESS -- status - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.commandResult( - {commandName="setReaderConfig", statusCode="success"}, - {state_change=true, visibility={displayed=false}} - ) - ) - ) - end -) - -test.register_coroutine_test( - "Handle Set Endpoint Key command and Clear Endpoint Key command received from SmartThings.", - function() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "setEndpointKey", - args = { - 0, - "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", - "nonEvictableEndpointKey", - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" - } - }, - } - ) - test.socket.matter:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetCredential( - mock_device, 1, -- endpoint - DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type - DoorLock.types.CredentialStruct( - { - credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, - credential_index = 1 - } - ), -- credential - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data - nil, -- user_index - nil, -- user_status - DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type - ), - } - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.SetCredentialResponse:build_test_command_response( - mock_device, 1, - DoorLock.types.DlStatus.SUCCESS, -- status - 1, -- user_index - 2 -- next_credential_index - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex=1, userType="adminMember"}}, - {visibility={displayed=false}} - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.credentials( - {{ - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", - keyIndex=1, - keyType="nonEvictableEndpointKey", - userIndex=1 - }}, - {visibility={displayed=false}} - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.commandResult( - { - commandName="setEndpointKey", - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", - statusCode="success", - userIndex=1 - }, - {state_change=true, visibility={displayed=false}} - ) - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "clearEndpointKey", - args = {1, "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", "nonEvictableEndpointKey"} - }, - } - ) - test.socket.matter:__expect_send( - { - mock_device.id, - DoorLock.server.commands.ClearCredential( - mock_device, 1, -- endpoint - DoorLock.types.CredentialStruct( - {credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, credential_index = 1} - ) - ), - } - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.server.commands.ClearCredential:build_test_command_response( - mock_device, 1 - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.credentials({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.commandResult( - { - commandName="clearEndpointKey", - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", - statusCode="success", - userIndex=1 - }, - {state_change=true, visibility={displayed=false}} - ) - ) - ) - end -) - -test.register_coroutine_test( - "Handle Set Issuer Key command and Clear Issuer Key command received from SmartThings.", - function() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "setIssuerKey", - args = { - 0, - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" - } - }, - } - ) - test.socket.matter:__expect_send( - { - mock_device.id, - DoorLock.server.commands.SetCredential( - mock_device, 1, -- endpoint - DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type - DoorLock.types.CredentialStruct( - { - credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, - credential_index = 1 - } - ), -- credential - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data - nil, -- user_index - nil, -- user_status - DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type - ), - } - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.client.commands.SetCredentialResponse:build_test_command_response( - mock_device, 1, - DoorLock.types.DlStatus.SUCCESS, -- status - 1, -- user_index - 2 -- next_credential_index - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - {{userIndex=1, userType="adminMember"}}, - {visibility={displayed=false}} - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.credentials( - {{ - keyIndex=1, - keyType="issuerKey", - userIndex=1 - }}, - {visibility={displayed=false}} - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.commandResult( - { - commandName="setIssuerKey", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", - statusCode="success", - userIndex=1 - }, - {state_change=true, visibility={displayed=false}} - ) - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive( - { - mock_device.id, - { - capability = capabilities.lockAliro.ID, - command = "clearIssuerKey", - args = {1, "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2"} - }, - } - ) - test.socket.matter:__expect_send( - { - mock_device.id, - DoorLock.server.commands.ClearCredential( - mock_device, 1, -- endpoint - DoorLock.types.CredentialStruct( - {credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, credential_index = 1} - ) - ), - } - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - DoorLock.server.commands.ClearCredential:build_test_command_response( - mock_device, 1 - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.credentials({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAliro.commandResult( - { - commandName="clearIssuerKey", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", - statusCode="success", - userIndex=1 - }, - {state_change=true, visibility={displayed=false}} - ) - ) - ) - end -) - -test.run_registered_tests() +-- Copyright 2023 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local DoorLock = clusters.DoorLock +local OctetString1 = require "st.matter.data_types.OctetString1" + +local enabled_optional_component_capability_pairs = {{ + "main", + { + capabilities.lockUsers.ID, + capabilities.lockSchedules.ID, + capabilities.lockAliro.ID + } +}} +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition( + "lock-modular.yml", + {enabled_optional_capabilities = enabled_optional_component_capability_pairs} + ), + manufacturer_info = { + vendor_id = 0x135D, + product_id = 0x00C1, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x2510, -- WDSCH & YDSCH & USR & ALIRO + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroReaderVerificationKey:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroReaderGroupIdentifier:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroReaderGroupSubIdentifier:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroGroupResolvingKey:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.AliroBLEAdvertisingVersion:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.NumberOfAliroEndpointKeysSupported:subscribe(mock_device)) + subscribe_request:merge(cluster_base.subscribe(mock_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Handle received AliroReaderVerificationKey from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroReaderVerificationKey:build_test_report_data( + mock_device, 1, + "\x04\xA9\xCB\xE4\x18\xEB\x09\x66\x16\x43\xE2\xA4\xA8\x46\xB8\xED\xFE\x27\x86\x98\x30\x2E\x9F\xB4\x3E\x9B\xFF\xD3\xE3\x10\xCC\x2C\x2C\x7F\xF4\x02\xE0\x6E\x40\xEA\x3C\xE1\x29\x43\x52\x73\x36\x68\x3F\xC5\xB1\xCB\x0C\x6A\x7C\x3F\x0B\x5A\xFF\x78\x35\xDF\x21\xC6\x24" + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.readerVerificationKey( + "04a9cbe418eb09661643e2a4a846b8edfe278698302e9fb43e9bffd3e310cc2c2c7ff402e06e40ea3ce12943527336683fc5b1cb0c6a7c3f0b5aff7835df21c624", + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroReaderGroupIdentifier from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroReaderGroupIdentifier:build_test_report_data( + mock_device, 1, + "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.readerGroupIdentifier( + "e24f1b205ba923b32cd13dc009e993a8", + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroExpeditedTransactionSupportedProtocolVersions from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:build_test_report_data( + mock_device, 1, + {OctetString1("\x00\x09"), OctetString1("\x01\x00")} + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.expeditedTransactionProtocolVersions( + {"0.9", "1.0"}, + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroSupportedBLEUWBProtocolVersions from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:build_test_report_data( + mock_device, 1, + {OctetString1("\x00\x09"), OctetString1("\x01\x00")} + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.bleUWBProtocolVersions( + {"0.9", "1.0"}, + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroReaderVerificationKey from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:build_test_report_data( + mock_device, 1, + 35 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.maxCredentialIssuerKeys( + 35, + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroGroupResolvingKey from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroGroupResolvingKey:build_test_report_data( + mock_device, 1, + "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.groupResolvingKey( + "e24f1b205ba923b32cd13dc009e993a8", + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received AliroBLEAdvertisingVersion from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.AliroBLEAdvertisingVersion:build_test_report_data( + mock_device, 1, + 1 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.bleAdvertisingVersion( + "1", + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle received NumberOfAliroEndpointKeysSupported from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.NumberOfAliroEndpointKeysSupported:build_test_report_data( + mock_device, 1, + 10 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.maxEndpointKeys( + 10, + {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle Set Card Id command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setCardId", + args = {"3icub18c8pr00"} + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.cardId("3icub18c8pr00", {visibility = {displayed = false}}) + ) + ) + end +) + +test.register_coroutine_test( + "Handle Set Reader Config command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setReaderConfig", + args = { + "1a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac", + "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", + "e24f1b205ba923b32cd13dc009e993a8", + nil + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetAliroReaderConfig( + mock_device, 1, -- endpoint + "\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC", + "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", + "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8", + nil + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.SetAliroReaderConfig:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.SUCCESS -- status + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + {commandName="setReaderConfig", statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end +) + +test.register_coroutine_test( + "Handle Set Endpoint Key command and Clear Endpoint Key command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setEndpointKey", + args = { + 0, + "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + "nonEvictableEndpointKey", + "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", + "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, + credential_index = 1 + } + ), -- credential + "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.SUCCESS, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex=1, userType="adminMember"}}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.credentials( + {{ + keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + keyIndex=1, + keyType="nonEvictableEndpointKey", + userIndex=1 + }}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setEndpointKey", + keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + statusCode="success", + userIndex=1 + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "clearEndpointKey", + args = {1, "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", "nonEvictableEndpointKey"} + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.ClearCredential( + mock_device, 1, -- endpoint + DoorLock.types.CredentialStruct( + {credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, credential_index = 1} + ) + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.ClearCredential:build_test_command_response( + mock_device, 1 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.credentials({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="clearEndpointKey", + keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + statusCode="success", + userIndex=1 + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end +) + +test.register_coroutine_test( + "Handle Set Issuer Key command and Clear Issuer Key command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setIssuerKey", + args = { + 0, + "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", + "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = 1 + } + ), -- credential + "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.SUCCESS, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + {{userIndex=1, userType="adminMember"}}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.credentials( + {{ + keyIndex=1, + keyType="issuerKey", + userIndex=1 + }}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setIssuerKey", + requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + statusCode="success", + userIndex=1 + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "clearIssuerKey", + args = {1, "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2"} + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.ClearCredential( + mock_device, 1, -- endpoint + DoorLock.types.CredentialStruct( + {credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, credential_index = 1} + ) + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.ClearCredential:build_test_command_response( + mock_device, 1 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.credentials({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="clearIssuerKey", + requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + statusCode="success", + userIndex=1 + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-pump/src/init.lua b/drivers/SmartThings/matter-pump/src/init.lua index db43d3b1df..0e79f66cbb 100644 --- a/drivers/SmartThings/matter-pump/src/init.lua +++ b/drivers/SmartThings/matter-pump/src/init.lua @@ -1,328 +1,329 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" -local MatterDriver = require "st.matter.driver" - -local IS_LOCAL_OVERRIDE = "__is_local_override" --- Per matter spec, the pump level is in steps of 0.5% and the --- max level value is 200. Anything above is considered 100% -local MAX_PUMP_ATTR_LEVEL = 200 -local MAX_CAP_SWITCH_LEVEL = 100 - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.PumpConfigurationAndControl = require "PumpConfigurationAndControl" -end - -local pumpOperationMode = capabilities.pumpOperationMode -local pumpControlMode = capabilities.pumpControlMode - -local PUMP_OPERATION_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.NORMAL] = pumpOperationMode.operationMode.normal, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MINIMUM] = pumpOperationMode.operationMode.minimum, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MAXIMUM] = pumpOperationMode.operationMode.maximum, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.LOCAL] = pumpOperationMode.operationMode.localSetting, -} - -local PUMP_CONTROL_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.controlMode.constantSpeed, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.controlMode.constantPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.controlMode.proportionalPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.controlMode.constantFlow, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.controlMode.constantTemperature, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.controlMode.automatic, -} - -local PUMP_CURRENT_CONTROL_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.currentControlMode.constantSpeed, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.currentControlMode.constantPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.currentControlMode.proportionalPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.currentControlMode.constantFlow, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.currentControlMode.constantTemperature, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.currentControlMode.automatic, -} - -local subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff, - }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel - }, - [capabilities.pumpOperationMode.ID]={ - clusters.PumpConfigurationAndControl.attributes.OperationMode, - clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode, - clusters.PumpConfigurationAndControl.attributes.PumpStatus, - }, - [capabilities.pumpControlMode.ID]={ - clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode, - }, -} - -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - local eps = embedded_cluster_utils.get_endpoints(device, cluster) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v - end - end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return res -end - -local function component_to_endpoint(device, component_name) - -- Use the find_default_endpoint function to return the first endpoint that - -- supports a given cluster. - return find_default_endpoint(device, clusters.PumpConfigurationAndControl.ID) -end - -local function device_init(driver, device) - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) -end - -local function info_changed(driver, device, event, args) - --Note this is needed because device:subscribe() does not recalculate - -- the subscribed attributes each time it is run, that only happens at init. - -- This will change in the 0.48.x release of the lua libs. - for cap_id, attributes in pairs(subscribed_attributes) do - if device:supports_capability_by_id(cap_id) then - for _, attr in ipairs(attributes) do - device:add_subscribed_attribute(attr) - end - end - end - device:subscribe() -end - -local function set_supported_op_mode(driver, device) - local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) - local local_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.LOCAL_OPERATION}) - local supported_op_modes = {pumpOperationMode.operationMode.normal.NAME} - if #spd_eps > 0 then - table.insert(supported_op_modes, pumpOperationMode.operationMode.minimum.NAME) - table.insert(supported_op_modes, pumpOperationMode.operationMode.maximum.NAME) - end - if #local_eps > 0 then - table.insert(supported_op_modes, pumpOperationMode.operationMode.localSetting.NAME) - end - device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) -end - -local function set_supported_control_mode(driver, device) - local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) - local prsconst_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_PRESSURE}) - local prscomp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.COMPENSATED_PRESSURE}) - local flw_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_FLOW}) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_TEMPERATURE}) - local auto_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.AUTOMATIC}) - local supported_control_modes = {} - if #spd_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantSpeed.NAME) - end - if #prsconst_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantPressure.NAME) - end - if #prscomp_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.proportionalPressure.NAME) - end - if #flw_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantFlow.NAME) - end - if #temp_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantTemperature.NAME) - end - if #auto_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.automatic.NAME) - end - device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) -end - -local function do_configure(driver, device) - local pump_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID) - local level_eps = embedded_cluster_utils.get_endpoints(device, clusters.LevelControl.ID) - local profile_name = "pump" - if #pump_eps == 1 then - if #level_eps > 0 then - profile_name = profile_name .. "-level" - else - profile_name = profile_name .. "-only" - end - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - else - device.log.warn_with({hub_logs=true}, "Device does not support pump configuration and control cluster") - end - set_supported_op_mode(driver, device) - set_supported_control_mode(driver, device) -end - --- Matter Handlers -- -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end -end - -local function level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - local level = math.floor((ib.data.value / MAX_PUMP_ATTR_LEVEL * MAX_CAP_SWITCH_LEVEL) + 0.5) - level = math.min(level, MAX_CAP_SWITCH_LEVEL) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) - end -end - -local function effective_operation_mode_handler(driver, device, ib, response) - local modeEnum = clusters.PumpConfigurationAndControl.types.OperationModeEnum - local supported_control_modes = {} - local local_override = device:get_field(IS_LOCAL_OVERRIDE) - if not local_override then - set_supported_op_mode(driver, device) - end - if ib.data.value == modeEnum.NORMAL then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.normal()) - set_supported_control_mode(driver, device) - elseif ib.data.value == modeEnum.MINIMUM then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.minimum()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == modeEnum.MAXIMUM then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.maximum()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == modeEnum.LOCAL then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - end -end - -local function effective_control_mode_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, PUMP_CURRENT_CONTROL_MODE_MAP[ib.data.value]()) -end - -local function pump_status_handler(driver, device, ib, response) - if ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.LOCAL_OVERRIDE then - device:set_field(IS_LOCAL_OVERRIDE, true, {persist = true}) - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) - local supported_op_modes = {} - local supported_control_modes = {} - device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) - device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.RUNNING then - device:set_field(IS_LOCAL_OVERRIDE, false, {persist = true}) - device:send(clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode:read(device)) - end -end - --- Capability Handlers -- -local function handle_switch_on(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function handle_set_level(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.level / MAX_CAP_SWITCH_LEVEL * MAX_PUMP_ATTR_LEVEL) - local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0 ,0) - device:send(req) -end - -local function set_operation_mode(driver, device, cmd) - local mode_id = nil - for id, mode in pairs(PUMP_OPERATION_MODE_MAP) do - if mode.NAME == cmd.args.operationMode then - mode_id = id - break - end - end - if mode_id then - device:send(clusters.PumpConfigurationAndControl.attributes.OperationMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) - end -end - -local function set_control_mode(driver, device, cmd) - local mode_id = nil - for id, mode in pairs(PUMP_CONTROL_MODE_MAP) do - if mode.NAME == cmd.args.controlMode then - mode_id = id - break - end - end - if mode_id then - device:send(clusters.PumpConfigurationAndControl.attributes.ControlMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) - end -end - -local matter_driver_template = { - lifecycle_handlers = { - init = device_init, - doConfigure = do_configure, - infoChanged = info_changed, - }, - matter_handlers = { - attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, - }, - [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler - }, - [clusters.PumpConfigurationAndControl.ID] = { - [clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode.ID] = effective_operation_mode_handler, - [clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode.ID] = effective_control_mode_handler, - [clusters.PumpConfigurationAndControl.attributes.PumpStatus.ID] = pump_status_handler, - }, - }, - }, - subscribed_attributes = subscribed_attributes, - capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, - }, - [capabilities.pumpOperationMode.ID] = { - [capabilities.pumpOperationMode.commands.setOperationMode.NAME] = set_operation_mode, - }, - [capabilities.pumpControlMode.ID] = { - [capabilities.pumpControlMode.commands.setControlMode.NAME] = set_control_mode, - }, - }, - supported_capabilities = { - capabilities.switch, - capabilities.switchLevel, - capabilities.pumpOperationMode, - capabilities.pumpControlMode, - }, -} - -local matter_driver = MatterDriver("matter-pump", matter_driver_template) -log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) -matter_driver:run() \ No newline at end of file +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local log = require "log" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "embedded-cluster-utils" +local MatterDriver = require "st.matter.driver" + +local IS_LOCAL_OVERRIDE = "__is_local_override" +-- Per matter spec, the pump level is in steps of 0.5% and the +-- max level value is 200. Anything above is considered 100% +local MAX_PUMP_ATTR_LEVEL = 200 +local MAX_CAP_SWITCH_LEVEL = 100 + +-- Include driver-side definitions when lua libs api version is < 10 +local version = require "version" +if version.api < 10 then + clusters.PumpConfigurationAndControl = require "PumpConfigurationAndControl" +end + +local pumpOperationMode = capabilities.pumpOperationMode +local pumpControlMode = capabilities.pumpControlMode + +local PUMP_OPERATION_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.NORMAL] = pumpOperationMode.operationMode.normal, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MINIMUM] = pumpOperationMode.operationMode.minimum, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MAXIMUM] = pumpOperationMode.operationMode.maximum, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.LOCAL] = pumpOperationMode.operationMode.localSetting, +} + +local PUMP_CONTROL_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.controlMode.constantSpeed, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.controlMode.constantPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.controlMode.proportionalPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.controlMode.constantFlow, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.controlMode.constantTemperature, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.controlMode.automatic, +} + +local PUMP_CURRENT_CONTROL_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.currentControlMode.constantSpeed, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.currentControlMode.constantPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.currentControlMode.proportionalPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.currentControlMode.constantFlow, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.currentControlMode.constantTemperature, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.currentControlMode.automatic, +} + +local subscribed_attributes = { + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff, + }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel + }, + [capabilities.pumpOperationMode.ID]={ + clusters.PumpConfigurationAndControl.attributes.OperationMode, + clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode, + clusters.PumpConfigurationAndControl.attributes.PumpStatus, + }, + [capabilities.pumpControlMode.ID]={ + clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode, + }, +} + +local function find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = embedded_cluster_utils.get_endpoints(device, cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return res +end + +local function component_to_endpoint(device, component_name) + -- Use the find_default_endpoint function to return the first endpoint that + -- supports a given cluster. + return find_default_endpoint(device, clusters.PumpConfigurationAndControl.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + --Note this is needed because device:subscribe() does not recalculate + -- the subscribed attributes each time it is run, that only happens at init. + -- This will change in the 0.48.x release of the lua libs. + for cap_id, attributes in pairs(subscribed_attributes) do + if device:supports_capability_by_id(cap_id) then + for _, attr in ipairs(attributes) do + device:add_subscribed_attribute(attr) + end + end + end + device:subscribe() +end + +local function set_supported_op_mode(driver, device) + local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) + local local_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.LOCAL_OPERATION}) + local supported_op_modes = {pumpOperationMode.operationMode.normal.NAME} + if #spd_eps > 0 then + table.insert(supported_op_modes, pumpOperationMode.operationMode.minimum.NAME) + table.insert(supported_op_modes, pumpOperationMode.operationMode.maximum.NAME) + end + if #local_eps > 0 then + table.insert(supported_op_modes, pumpOperationMode.operationMode.localSetting.NAME) + end + device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) +end + +local function set_supported_control_mode(driver, device) + local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) + local prsconst_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_PRESSURE}) + local prscomp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.COMPENSATED_PRESSURE}) + local flw_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_FLOW}) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_TEMPERATURE}) + local auto_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.AUTOMATIC}) + local supported_control_modes = {} + if #spd_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantSpeed.NAME) + end + if #prsconst_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantPressure.NAME) + end + if #prscomp_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.proportionalPressure.NAME) + end + if #flw_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantFlow.NAME) + end + if #temp_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantTemperature.NAME) + end + if #auto_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.automatic.NAME) + end + device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) +end + +local function do_configure(driver, device) + local pump_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID) + local level_eps = embedded_cluster_utils.get_endpoints(device, clusters.LevelControl.ID) + local profile_name = "pump" + if #pump_eps == 1 then + if #level_eps > 0 then + profile_name = profile_name .. "-level" + else + profile_name = profile_name .. "-only" + end + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + else + device.log.warn_with({hub_logs=true}, "Device does not support pump configuration and control cluster") + end + set_supported_op_mode(driver, device) + set_supported_control_mode(driver, device) +end + +-- Matter Handlers -- +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +local function level_attr_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local level = math.floor((ib.data.value / MAX_PUMP_ATTR_LEVEL * MAX_CAP_SWITCH_LEVEL) + 0.5) + level = math.min(level, MAX_CAP_SWITCH_LEVEL) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) + end +end + +local function effective_operation_mode_handler(driver, device, ib, response) + local modeEnum = clusters.PumpConfigurationAndControl.types.OperationModeEnum + local supported_control_modes = {} + local local_override = device:get_field(IS_LOCAL_OVERRIDE) + if not local_override then + set_supported_op_mode(driver, device) + end + if ib.data.value == modeEnum.NORMAL then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.normal()) + set_supported_control_mode(driver, device) + elseif ib.data.value == modeEnum.MINIMUM then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.minimum()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == modeEnum.MAXIMUM then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.maximum()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == modeEnum.LOCAL then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + end +end + +local function effective_control_mode_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, PUMP_CURRENT_CONTROL_MODE_MAP[ib.data.value]()) +end + +local function pump_status_handler(driver, device, ib, response) + if ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.LOCAL_OVERRIDE then + device:set_field(IS_LOCAL_OVERRIDE, true, {persist = true}) + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) + local supported_op_modes = {} + local supported_control_modes = {} + device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) + device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.RUNNING then + device:set_field(IS_LOCAL_OVERRIDE, false, {persist = true}) + device:send(clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode:read(device)) + end +end + +-- Capability Handlers -- +local function handle_switch_on(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.OnOff.server.commands.On(device, endpoint_id) + device:send(req) +end + +local function handle_switch_off(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.OnOff.server.commands.Off(device, endpoint_id) + device:send(req) +end + +local function handle_set_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = math.floor(cmd.args.level / MAX_CAP_SWITCH_LEVEL * MAX_PUMP_ATTR_LEVEL) + local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0 ,0) + device:send(req) +end + +local function set_operation_mode(driver, device, cmd) + local mode_id = nil + for id, mode in pairs(PUMP_OPERATION_MODE_MAP) do + if mode.NAME == cmd.args.operationMode then + mode_id = id + break + end + end + if mode_id then + device:send(clusters.PumpConfigurationAndControl.attributes.OperationMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) + end +end + +local function set_control_mode(driver, device, cmd) + local mode_id = nil + for id, mode in pairs(PUMP_CONTROL_MODE_MAP) do + if mode.NAME == cmd.args.controlMode then + mode_id = id + break + end + end + if mode_id then + device:send(clusters.PumpConfigurationAndControl.attributes.ControlMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) + end +end + +local matter_driver_template = { + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [clusters.LevelControl.ID] = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler + }, + [clusters.PumpConfigurationAndControl.ID] = { + [clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode.ID] = effective_operation_mode_handler, + [clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode.ID] = effective_control_mode_handler, + [clusters.PumpConfigurationAndControl.attributes.PumpStatus.ID] = pump_status_handler, + }, + }, + }, + subscribed_attributes = subscribed_attributes, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_switch_on, + [capabilities.switch.commands.off.NAME] = handle_switch_off, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, + }, + [capabilities.pumpOperationMode.ID] = { + [capabilities.pumpOperationMode.commands.setOperationMode.NAME] = set_operation_mode, + }, + [capabilities.pumpControlMode.ID] = { + [capabilities.pumpControlMode.commands.setControlMode.NAME] = set_control_mode, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.switchLevel, + capabilities.pumpOperationMode, + capabilities.pumpControlMode, + }, + shared_device_thread_enabled = true, +} + +local matter_driver = MatterDriver("matter-pump", matter_driver_template) +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 1e52f321ee..2badac1413 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -120,6 +120,7 @@ local function driver_switched(driver, device) match_profile(driver, device) device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end local function info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua index 3b03b1a2e1..626f7fea60 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -218,6 +218,19 @@ local function operating_state_init() ) end +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + test.socket.matter:__expect_send({mock_device.id, clusters.RvcOperationalState.attributes.AcceptedCommandList:read()}) + mock_device:expect_metadata_update({ profile = "rvc-clean-mode-service-area" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Assert profile applied over doConfigure", function() diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index e3b483cad5..45b21dd386 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -15,6 +15,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2005 deviceProfileName: presence-illuminance-temperature-humidity-battery + - id: "4447/8201" + deviceLabel: Spatial Multi-Sensor FP400 + vendorId: 0x115F + productId: 0x2009 + deviceProfileName: aqara-fp400 #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" @@ -350,3 +355,8 @@ matterGeneric: deviceTypes: - id: 0x0306 # Flow Sensor deviceProfileName: flow-battery + - id: "matter/soil/sensor" + deviceLabel: Matter Soil Sensor + deviceTypes: + - id: 0x0045 # Soil Sensor + deviceProfileName: soil-sensor-battery diff --git a/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml b/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml new file mode 100644 index 0000000000..c2d5b7b037 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml @@ -0,0 +1,14 @@ +name: aqara-fp400 +components: +- id: main + capabilities: + - id: presenceSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: PresenceSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml new file mode 100644 index 0000000000..07c9162e4d --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml @@ -0,0 +1,17 @@ +name: soil-sensor-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml new file mode 100644 index 0000000000..cfda9b9e95 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml @@ -0,0 +1,17 @@ +name: soil-sensor-batteryLevel +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml new file mode 100644 index 0000000000..280edc80c4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml @@ -0,0 +1,15 @@ +name: soil-sensor +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml new file mode 100644 index 0000000000..73db53e7c1 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml @@ -0,0 +1,21 @@ +name: temperature-soil-sensor-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml new file mode 100644 index 0000000000..55ccc8fb25 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml @@ -0,0 +1,21 @@ +name: temperature-soil-sensor-batteryLevel +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml new file mode 100644 index 0000000000..bc327f01b0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml @@ -0,0 +1,19 @@ +name: temperature-soil-sensor +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua new file mode 100644 index 0000000000..98c23abef4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua @@ -0,0 +1,5 @@ +local GlobalTypes = require "embedded_clusters.Global.types" + +local Global = {} +Global.types = GlobalTypes +return Global diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua new file mode 100644 index 0000000000..ed8b969b49 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LevelValueEnum = {} +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.LOW] = "LOW", + [self.MEDIUM] = "MEDIUM", + [self.HIGH] = "HIGH", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.LOW = 0x01 +new_mt.__index.MEDIUM = 0x02 +new_mt.__index.HIGH = 0x03 +new_mt.__index.CRITICAL = 0x04 + +LevelValueEnum.UNKNOWN = 0x00 +LevelValueEnum.LOW = 0x01 +LevelValueEnum.MEDIUM = 0x02 +LevelValueEnum.HIGH = 0x03 +LevelValueEnum.CRITICAL = 0x04 + +LevelValueEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LevelValueEnum, new_mt) + +return LevelValueEnum diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua new file mode 100644 index 0000000000..b298a66961 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua @@ -0,0 +1,119 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local MeasurementAccuracyRangeStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyRangeStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyRangeStruct.field_defs = { + { + name = "range_min", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "range_max", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "percent_max", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "percent_min", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "percent_typical", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "fixed_max", + field_id = 5, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "fixed_min", + field_id = 6, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "fixed_typical", + field_id = 7, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, +} + +MeasurementAccuracyRangeStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for _idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif not (field_def.is_optional and tbl[field_def.name] == nil) then + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyRangeStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyRangeStruct.init +new_mt.__index.serialize = MeasurementAccuracyRangeStruct.serialize + +MeasurementAccuracyRangeStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyRangeStruct, new_mt) + +return MeasurementAccuracyRangeStruct diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua new file mode 100644 index 0000000000..4da8857164 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua @@ -0,0 +1,99 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local MeasurementAccuracyStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyStruct.field_defs = { + { + name = "measurement_type", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "embedded_clusters.Global.types.MeasurementTypeEnum", + }, + { + name = "measured", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "min_measured_value", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "max_measured_value", + field_id = 3, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "accuracy_ranges", + field_id = 4, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.Global.types.MeasurementAccuracyRangeStruct", + }, +} + +MeasurementAccuracyStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for _idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif not (field_def.is_optional and tbl[field_def.name] == nil) then + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyStruct.init +new_mt.__index.serialize = MeasurementAccuracyStruct.serialize + +MeasurementAccuracyStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyStruct, new_mt) + +return MeasurementAccuracyStruct diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua new file mode 100644 index 0000000000..2e749ebacd --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua @@ -0,0 +1,75 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementTypeEnum = {} +local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNSPECIFIED] = "UNSPECIFIED", + [self.VOLTAGE] = "VOLTAGE", + [self.ACTIVE_CURRENT] = "ACTIVE_CURRENT", + [self.REACTIVE_CURRENT] = "REACTIVE_CURRENT", + [self.APPARENT_CURRENT] = "APPARENT_CURRENT", + [self.ACTIVE_POWER] = "ACTIVE_POWER", + [self.REACTIVE_POWER] = "REACTIVE_POWER", + [self.APPARENT_POWER] = "APPARENT_POWER", + [self.RMS_VOLTAGE] = "RMS_VOLTAGE", + [self.RMS_CURRENT] = "RMS_CURRENT", + [self.RMS_POWER] = "RMS_POWER", + [self.FREQUENCY] = "FREQUENCY", + [self.POWER_FACTOR] = "POWER_FACTOR", + [self.NEUTRAL_CURRENT] = "NEUTRAL_CURRENT", + [self.ELECTRICAL_ENERGY] = "ELECTRICAL_ENERGY", + [self.REACTIVE_ENERGY] = "REACTIVE_ENERGY", + [self.APPARENT_ENERGY] = "APPARENT_ENERGY", + [self.SOIL_MOISTURE] = "SOIL_MOISTURE", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNSPECIFIED = 0x00 +new_mt.__index.VOLTAGE = 0x01 +new_mt.__index.ACTIVE_CURRENT = 0x02 +new_mt.__index.REACTIVE_CURRENT = 0x03 +new_mt.__index.APPARENT_CURRENT = 0x04 +new_mt.__index.ACTIVE_POWER = 0x05 +new_mt.__index.REACTIVE_POWER = 0x06 +new_mt.__index.APPARENT_POWER = 0x07 +new_mt.__index.RMS_VOLTAGE = 0x08 +new_mt.__index.RMS_CURRENT = 0x09 +new_mt.__index.RMS_POWER = 0x0A +new_mt.__index.FREQUENCY = 0x0B +new_mt.__index.POWER_FACTOR = 0x0C +new_mt.__index.NEUTRAL_CURRENT = 0x0D +new_mt.__index.ELECTRICAL_ENERGY = 0x0E +new_mt.__index.REACTIVE_ENERGY = 0x0F +new_mt.__index.APPARENT_ENERGY = 0x10 +new_mt.__index.SOIL_MOISTURE = 0x11 + +MeasurementTypeEnum.UNSPECIFIED = 0x00 +MeasurementTypeEnum.VOLTAGE = 0x01 +MeasurementTypeEnum.ACTIVE_CURRENT = 0x02 +MeasurementTypeEnum.REACTIVE_CURRENT = 0x03 +MeasurementTypeEnum.APPARENT_CURRENT = 0x04 +MeasurementTypeEnum.ACTIVE_POWER = 0x05 +MeasurementTypeEnum.REACTIVE_POWER = 0x06 +MeasurementTypeEnum.APPARENT_POWER = 0x07 +MeasurementTypeEnum.RMS_VOLTAGE = 0x08 +MeasurementTypeEnum.RMS_CURRENT = 0x09 +MeasurementTypeEnum.RMS_POWER = 0x0A +MeasurementTypeEnum.FREQUENCY = 0x0B +MeasurementTypeEnum.POWER_FACTOR = 0x0C +MeasurementTypeEnum.NEUTRAL_CURRENT = 0x0D +MeasurementTypeEnum.ELECTRICAL_ENERGY = 0x0E +MeasurementTypeEnum.REACTIVE_ENERGY = 0x0F +MeasurementTypeEnum.APPARENT_ENERGY = 0x10 +MeasurementTypeEnum.SOIL_MOISTURE = 0x11 + +MeasurementTypeEnum.augment_type = function(_cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementTypeEnum, new_mt) + +return MeasurementTypeEnum diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua new file mode 100644 index 0000000000..984c5e02d8 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua @@ -0,0 +1,14 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.Global.types." .. key) + end + return types_mt.__types_cache[key] +end + +local GlobalTypes = {} + +setmetatable(GlobalTypes, types_mt) + +return GlobalTypes diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua new file mode 100644 index 0000000000..a7db91daea --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua @@ -0,0 +1,46 @@ +local cluster_base = require "st.matter.cluster_base" +local SoilMeasurementServerAttributes = require "embedded_clusters.SoilMeasurement.server.attributes" + +local SoilMeasurement = {} + +SoilMeasurement.ID = 0x0430 +SoilMeasurement.NAME = "SoilMeasurement" +SoilMeasurement.server = {} +SoilMeasurement.client = {} +SoilMeasurement.server.attributes = SoilMeasurementServerAttributes:set_parent_cluster(SoilMeasurement) + +function SoilMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SoilMoistureMeasurementLimits", + [0x0001] = "SoilMoistureMeasuredValue", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +SoilMeasurement.attribute_direction_map = { + ["SoilMoistureMeasurementLimits"] = "server", + ["SoilMoistureMeasuredValue"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = SoilMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, SoilMeasurement.NAME)) + end + return SoilMeasurement[direction].attributes[key] +end +SoilMeasurement.attributes = {} +setmetatable(SoilMeasurement.attributes, attribute_helper_mt) + +setmetatable(SoilMeasurement, {__index = cluster_base}) + +return SoilMeasurement diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua new file mode 100644 index 0000000000..5df92ec62a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SoilMoistureMeasuredValue = { + ID = 0x0001, + NAME = "SoilMoistureMeasuredValue", + base_type = require "st.matter.data_types.Uint8", +} + +function SoilMoistureMeasuredValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function SoilMoistureMeasuredValue:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasuredValue:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SoilMoistureMeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SoilMoistureMeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(SoilMoistureMeasuredValue, {__call = SoilMoistureMeasuredValue.new_value, __index = SoilMoistureMeasuredValue.base_type}) +return SoilMoistureMeasuredValue diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua new file mode 100644 index 0000000000..6fbf1a8c8a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SoilMoistureMeasurementLimits = { + ID = 0x0000, + NAME = "SoilMoistureMeasurementLimits", + base_type = require "embedded_clusters.Global.types.MeasurementAccuracyStruct", +} + +function SoilMoistureMeasurementLimits:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SoilMoistureMeasurementLimits:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasurementLimits:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasurementLimits:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SoilMoistureMeasurementLimits:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SoilMoistureMeasurementLimits:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SoilMoistureMeasurementLimits, {__call = SoilMoistureMeasurementLimits.new_value, __index = SoilMoistureMeasurementLimits.base_type}) +return SoilMoistureMeasurementLimits diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3741efd72a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.SoilMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local SoilMeasurementServerAttributes = {} + +function SoilMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(SoilMeasurementServerAttributes, attr_mt) + +return SoilMeasurementServerAttributes diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 73e34fcd56..6dbd6d9811 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -37,6 +37,12 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +-- Include driver-side definitions when lua libs api version is < 21 +-- TODO: change this to < 20 once the lua libs have been updated for hub-core 61 +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + local SensorLifecycleHandlers = {} function SensorLifecycleHandlers.do_configure(driver, device) @@ -117,6 +123,10 @@ local matter_driver_template = { [clusters.RelativeHumidityMeasurement.ID] = { [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler }, + [clusters.SoilMeasurement.ID] = { + [clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue.ID] = attribute_handlers.soil_moisture_measured_value_handler, + [clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits.ID] = attribute_handlers.soil_moisture_measurement_limits_handler + }, [clusters.TemperatureMeasurement.ID] = { [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), @@ -164,7 +174,9 @@ local matter_driver_template = { clusters.BooleanState.attributes.StateValue, }, [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits }, [capabilities.temperatureAlarm.ID] = { clusters.BooleanState.attributes.StateValue, @@ -243,9 +255,6 @@ local matter_driver_template = { clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, [capabilities.tvocHealthConcern.ID] = { clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue }, @@ -296,6 +305,7 @@ local matter_driver_template = { capabilities.flowMeasurement, }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-sensor", matter_driver_template) diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua index 9bba480855..a81a03b4a6 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -9,6 +9,10 @@ local fields = require "sensor_utils.fields" local device_cfg = require "sensor_utils.device_configuration" local version = require "version" +if version.api < 13 then + clusters.Global = require "embedded_clusters.Global" +end + local AttributeHandlers = {} @@ -69,16 +73,39 @@ function AttributeHandlers.humidity_measured_value_handler(driver, device, ib, r end +-- [[ SOIL MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.soil_moisture_measured_value_handler(driver, device, ib, response) + if ib.data.value == nil then return end + local min = sensor_utils.get_field_for_endpoint(device, fields.SOIL_LIMIT_MIN, ib.endpoint_id) or sensor_utils.SOIL_MOISTURE_MIN + local max = sensor_utils.get_field_for_endpoint(device, fields.SOIL_LIMIT_MAX, ib.endpoint_id) or sensor_utils.SOIL_MOISTURE_MAX + local soil_moisture = st_utils.clamp_value(ib.data.value, min, max) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(soil_moisture)) +end + +function AttributeHandlers.soil_moisture_measurement_limits_handler(driver, device, ib, response) + local MeasurementAccuracyStruct = require "embedded_clusters.Global.types.MeasurementAccuracyStruct" + MeasurementAccuracyStruct:augment_type(ib.data) + local min_val = ib.data.elements and ib.data.elements.min_measured_value and ib.data.elements.min_measured_value.value + local max_val = ib.data.elements and ib.data.elements.max_measured_value and ib.data.elements.max_measured_value.value + if not (min_val and max_val) or (min_val >= max_val) or (min_val < sensor_utils.SOIL_MOISTURE_MIN) or (max_val > sensor_utils.SOIL_MOISTURE_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported invalid soil moisture limits: min=%d, max=%d", min_val, max_val)) + end + sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MIN, ib.endpoint_id, min_val) + sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MAX, ib.endpoint_id, max_val) +end + + -- [[ BOOLEAN STATE CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.boolean_state_value_handler(driver, device, ib, response) local name for dt_name, _ in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do - local dt_ep_id = device:get_field(dt_name) - if ib.endpoint_id == dt_ep_id then - name = dt_name - break - end + local dt_ep_id = device:get_field(dt_name) + if ib.endpoint_id == dt_ep_id then + name = dt_name + break + end end if name then device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua index 2c5f38524a..4690a3e356 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua @@ -11,18 +11,22 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + local DeviceConfiguration = {} function DeviceConfiguration.set_boolean_device_type_per_endpoint(driver, device) for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do - if dt.device_type_id == info.id then - device:set_field(dt_name, ep.endpoint_id, { persist = true }) - device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) - end - end + for _, dt in ipairs(ep.device_types) do + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + if dt.device_type_id == info.id then + device:set_field(dt_name, ep.endpoint_id, { persist = true }) + device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) + end end + end end end @@ -53,12 +57,18 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) profile_name = profile_name .. "-illuminance" end - if device:supports_capability(capabilities.temperatureMeasurement) then + if device:supports_capability(capabilities.temperatureMeasurement) or + #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then profile_name = profile_name .. "-temperature" end if device:supports_capability(capabilities.relativeHumidityMeasurement) then - profile_name = profile_name .. "-humidity" + if #embedded_cluster_utils.get_endpoints(device, clusters.SoilMeasurement.ID) > 0 then + -- TODO: Update soil sensor profiles to use the SoilSensor category once it is available. + profile_name = profile_name .. "-soil-sensor" + else + profile_name = profile_name .. "-humidity" + end end if device:supports_capability(capabilities.atmosphericPressureMeasurement) then @@ -107,7 +117,7 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) if #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.ACTIVE_INFRARED}) > 0 or #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RADAR}) > 0 or #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RF_SENSING}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) then + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) > 0 then occupancy_support = "-presence" end end @@ -117,8 +127,7 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) -- remove leading "-" profile_name = string.sub(profile_name, 2) - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) end -return DeviceConfiguration \ No newline at end of file +return DeviceConfiguration diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua index e2384098d9..37623d57d2 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua @@ -25,6 +25,10 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + local embedded_cluster_utils = {} local embedded_clusters_api_10 = { @@ -46,38 +50,44 @@ local embedded_clusters_api_11 = { [clusters.BooleanStateConfiguration.ID] = clusters.BooleanStateConfiguration } +local embedded_clusters_api_21 = { + [clusters.SoilMeasurement.ID] = clusters.SoilMeasurement +} + function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) - -- If using older lua libs and need to check for an embedded cluster feature, - -- we must use the embedded cluster definitions here - if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or - version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil then - local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] - local opts = opts or {} - if utils.table_size(opts) > 1 then - device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") - return - end - local clus_has_features = function(clus, feature_bitmap) - if not feature_bitmap or not clus then return false end - return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) - end - local eps = {} - for _, ep in ipairs(device.endpoints) do - for _, clus in ipairs(ep.clusters) do - if ((clus.cluster_id == cluster_id) - and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) - and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") - or (opts.cluster_type == clus.cluster_type)) - or (cluster_id == nil)) then - table.insert(eps, ep.endpoint_id) - if cluster_id == nil then break end - end + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or + version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil or + version.api < 21 and embedded_clusters_api_21[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] or + embedded_clusters_api_21[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end end end - return eps - else - return device:get_endpoints(cluster_id, opts) end + return eps + else + return device:get_endpoints(cluster_id, opts) end +end - return embedded_cluster_utils \ No newline at end of file +return embedded_cluster_utils diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua index f0b2a5c7a6..232e431277 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua @@ -11,6 +11,8 @@ SensorFields.TEMP_MAX = "__temp_max" SensorFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" SensorFields.FLOW_MIN = "__flow_min" SensorFields.FLOW_MAX = "__flow_max" +SensorFields.SOIL_LIMIT_MIN = "__soil_limit_min" +SensorFields.SOIL_LIMIT_MAX = "__soil_limit_max" SensorFields.battery_support = { NO_BATTERY = "NO_BATTERY", @@ -47,4 +49,10 @@ SensorFields.BOOLEAN_CAP_EVENT_MAP = { } } +SensorFields.vendor_overrides = { + [0x115F] = { -- AQARA_MANUFACTURER_ID + [0x2009] = { is_aqara_fp400 = true } + } +} + return SensorFields diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua index 5a0421fb0c..513c9f4602 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua @@ -1,8 +1,14 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local fields = require "sensor_utils.fields" + local utils = {} +-- Sanity check bounds for soil moisture measurement limits (percent) +utils.SOIL_MOISTURE_MIN = 0 +utils.SOIL_MOISTURE_MAX = 100 + function utils.get_field_for_endpoint(device, field, endpoint) return device:get_field(string.format("%s_%d", field, endpoint)) end @@ -11,6 +17,15 @@ function utils.set_field_for_endpoint(device, field, endpoint, value, additional device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) end +function utils.get_product_override_field(device, override_key) + if device.manufacturer_info + and fields.vendor_overrides[device.manufacturer_info.vendor_id] + and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id] + then + return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key] + end +end + function utils.tbl_contains(array, value) if value == nil then return false end for _, element in pairs(array or {}) do diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua index 3bf4c46f73..aa8b046927 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua @@ -6,5 +6,6 @@ local sub_drivers = { lazy_load_if_possible("sub_drivers.air_quality_sensor"), lazy_load_if_possible("sub_drivers.smoke_co_alarm"), lazy_load_if_possible("sub_drivers.bosch_button_contact"), + lazy_load_if_possible("sub_drivers.aqara_fp400"), } return sub_drivers diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua index e8d0cd4701..383af643a6 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua @@ -53,6 +53,7 @@ function AirQualitySensorLifecycleHandlers.driver_switched(driver, device) local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" legacy_device_cfg.match_profile(device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function AirQualitySensorLifecycleHandlers.device_init(driver, device) diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/can_handle.lua new file mode 100644 index 0000000000..d28a66f147 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_fp400(opts, driver, device) + local sensor_utils = require "sensor_utils.utils" + if sensor_utils.get_product_override_field(device, "is_aqara_fp400") then + return true, require("sub_drivers.aqara_fp400") + end + return false +end + +return is_aqara_fp400 diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua new file mode 100644 index 0000000000..5f47c86047 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua @@ -0,0 +1,23 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local Fp400LifecycleHandlers = {} + +-- overwrite to avoid unnecessary metadata update calls +function Fp400LifecycleHandlers.do_configure() end + +-- overwrite to avoid unnecessary metadata update calls +function Fp400LifecycleHandlers.driver_switched(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end + +local aqara_fp400_handler = { + NAME = "aqara-fp400", + lifecycle_handlers = { + doConfigure = Fp400LifecycleHandlers.do_configure, + driverSwitched = Fp400LifecycleHandlers.driver_switched, + }, + can_handle = require("sub_drivers.aqara_fp400.can_handle"), +} + +return aqara_fp400_handler diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua index ccbcbc74a0..27921e0dec 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua @@ -334,6 +334,28 @@ local function test_aqs_device_type_update_modular_profile(generic_mock_device, test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_modular_fingerprint.id, "driverSwitched" }) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) + mock_device_modular_fingerprint:expect_metadata_update({ profile = "aqs-modular", optional_component_capabilities = {{"main", {"tvocMeasurement"}}} }) + mock_device_modular_fingerprint:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = test_init_modular_fingerprint, + } +) + test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - all clusters", function() @@ -456,5 +478,40 @@ test.register_coroutine_test( { test_init = test_init_modular_fingerprint } ) +test.register_coroutine_test( + "Component-capability update without profile ID update should cause re-subscribe in infoChanged handler", + function() + local expected_metadata_modular_disabled = { + optional_component_capabilities={ + { + "main", + { + "tvocMeasurement", + }, + }, + }, + profile="aqs-modular", + } + local subscribe_request_tvoc = get_subscribe_request_tvoc() + local updated_device_profile = t_utils.get_profile_definition("aqs-modular.yml", + {enabled_optional_capabilities = expected_metadata_modular_disabled.optional_component_capabilities} + ) + updated_device_profile.id = "00000000-1111-2222-3333-000000000006" + test.socket.device_lifecycle:__queue_receive(mock_device_modular_fingerprint:generate_info_changed({ profile = updated_device_profile })) + test.socket.capability:__expect_send(mock_device_modular_fingerprint:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, subscribe_request_tvoc}) + end, + { test_init = test_init_modular_fingerprint } +) + +test.register_coroutine_test( + "No component-capability update and no profile ID update should not cause a re-subscribe in infoChanged handler", + function() + -- simulate no actual change + test.socket.device_lifecycle:__queue_receive(mock_device_modular_fingerprint:generate_info_changed({})) + end, + { test_init = test_init_modular_fingerprint } +) + -- run tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua new file mode 100644 index 0000000000..7f384c744e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua @@ -0,0 +1,141 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" + +local matter_endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.IlluminanceMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } + } +} + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("aqara-fp400.yml"), + manufacturer_info = { + vendor_id = 0x115F, + product_id = 0x2009, + }, + endpoints = matter_endpoints +}) + +local function subscribe_on_init(dev) + local subscribe_request = clusters.OccupancySensing.attributes.Occupancy:subscribe(dev) + subscribe_request:merge(clusters.IlluminanceMeasurement.attributes.MeasuredValue:subscribe(dev)) + return subscribe_request +end + +local function test_init() + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = subscribe_on_init(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test no profile change on doConfigure for FP400", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + -- The FP400 sub-driver overrides doConfigure to be a no-op + -- When doConfigure completes successfully, the framework automatically provisions the device + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Test no profile change on driverSwitched for FP400", + function() + local current_profile = mock_device.profile.id + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + -- The FP400 sub-driver overrides driverSwitched to only update provisioning state + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + -- Ensure profile has not changed + test.wait_for_events() + assert(mock_device.profile.id == current_profile, "Profile should not change on driverSwitched") + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Occupancy reports should generate correct presence messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 1, 1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.presenceSensor.presence("present")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 1, 0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.presenceSensor.presence("not present")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Illuminance reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 21370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + } + }, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua new file mode 100644 index 0000000000..0c845fa47e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua @@ -0,0 +1,221 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local version = require "version" + +clusters.Global = require "embedded_clusters.Global" + +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("temperature-soil-sensor.yml"), + manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.SoilMeasurement.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0045, device_type_revision = 1 } -- Soil Sensor + } + }, + } +}) + +local subscribe_request + +local cluster_subscribe_list = { + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue +} + +local function test_init() + subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "temperature-soil-sensor" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test infoChanged lifecycle event", + function() + local updated_device_profile = t_utils.get_profile_definition("temperature-soil-sensor.yml") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + end +) + +test.register_coroutine_test( + "Relative humidity reports should generate correct messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4049) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4050) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 41 })) + ) + end +) + +test.register_coroutine_test( + "Temperature reports should generate correct messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 40*100) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) + ) + end +) + +test.register_coroutine_test( + "Min and max temperature attributes set capability constraint", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_device, 1, 500) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_device, 1, 4000) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = 5.00, maximum = 40.00 }, unit = "C" }) + ) + ) + end +) + +test.register_coroutine_test( + "Soil moisture is reported raw when no limits are set", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 55) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 55 })) + ) + end +) + +local function build_soil_moisture_limits(min_value, max_value) + return clusters.Global.types.MeasurementAccuracyStruct({ + measurement_type = clusters.Global.types.MeasurementTypeEnum.SOIL_MOISTURE, + measured = true, + min_measured_value = min_value, + max_measured_value = max_value, + accuracy_ranges = {clusters.Global.types.MeasurementAccuracyRangeStruct({range_min = min_value, range_max = max_value})} + }) +end + +test.register_coroutine_test( + "Soil moisture is scaled 0-100% when min and max limits are set", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits:build_test_report_data(mock_device, 1, build_soil_moisture_limits(0, 50)) + } + ) + -- Receive a measured value of 25, which is 50% when scaled between 0 and 50 + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 25) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 25 })) + ) + end +) + +test.register_coroutine_test( + "Soil moisture scaling rounds correctly", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits:build_test_report_data(mock_device, 1, build_soil_moisture_limits(10, 90)) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 10) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 10 })) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 90) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 90 })) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index ce386a5a8b..e4602b4178 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -155,7 +155,7 @@ matterManufacturer: deviceLabel: OREIN Bath Fan OLO5S vendorId: 0x1396 productId: 0x1001 - deviceProfileName: light-color-level-fan + deviceProfileName: fan-modular - id: "5014/4214" deviceLabel: Linkind Smart Light Bulb vendorId: 0x1396 @@ -237,6 +237,11 @@ matterManufacturer: vendorId: 0x1209 productId: 0x3016 deviceProfileName: plug-power-energy-powerConsumption + - id: 4617/12307 + deviceLabel: "Motion Detector II [M]" + vendorId: 0x1209 + productId: 0x3013 + deviceProfileName: light-level-battery-illuminance-motion-temperature #Chengdu - id: "5218/8197" deviceLabel: Magic Cube DS001 @@ -1346,172 +1351,172 @@ matterManufacturer: deviceLabel: LIFX Supercolor (A19) vendorId: 0x1423 productId: 0x00A3 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/118" deviceLabel: LIFX Lightstrip vendorId: 0x1423 productId: 0x0076 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/221" deviceLabel: LIFX Spot vendorId: 0x1423 productId: 0x00DD - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/144" deviceLabel: LIFX String vendorId: 0x1423 productId: 0x0090 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/216" deviceLabel: LIFX Candle Color (B10) vendorId: 0x1423 productId: 0x00D8 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/225" deviceLabel: LIFX PAR38 vendorId: 0x1423 productId: 0x00E1 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/186" deviceLabel: LIFX Candle Color vendorId: 0x1423 productId: 0x00BA - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/202" deviceLabel: LIFX Ceiling 13x26 vendorId: 0x1423 productId: 0x00CA - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/143" deviceLabel: LIFX String vendorId: 0x1423 productId: 0x008F - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/166" deviceLabel: LIFX Supercolour (BR30) vendorId: 0x1423 productId: 0x00A6 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/167" deviceLabel: LIFX Downlight vendorId: 0x1423 productId: 0x00A7 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/207" deviceLabel: LIFX Everyday Lightstrip vendorId: 0x1423 productId: 0x00CF - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/222" deviceLabel: LIFX Path (Round) vendorId: 0x1423 productId: 0x00DE - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/203" deviceLabel: LIFX String vendorId: 0x1423 productId: 0x00CB - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/218" deviceLabel: LIFX Tube vendorId: 0x1423 productId: 0x00DA - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/214" deviceLabel: LIFX Permanent Outdoor vendorId: 0x1423 productId: 0x00D6 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/117" deviceLabel: LIFX Lightstrip vendorId: 0x1423 productId: 0x0075 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/223" deviceLabel: LIFX Downlight (6 Retro Downlight) vendorId: 0x1423 productId: 0x00DF - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/224" deviceLabel: LIFX Downlight (90mm Downlight) vendorId: 0x1423 productId: 0x00E0 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/204" deviceLabel: LIFX String vendorId: 0x1423 productId: 0x00CC - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/206" deviceLabel: LIFX Neon vendorId: 0x1423 productId: 0x00CE - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/164" deviceLabel: LIFX Supercolor (BR30) vendorId: 0x1423 productId: 0x00A4 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/120" deviceLabel: LIFX Beam vendorId: 0x1423 productId: 0x0078 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/208" deviceLabel: LIFX Everyday Lightstrip vendorId: 0x1423 productId: 0x00D0 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/165" deviceLabel: LIFX Supercolour (A19) vendorId: 0x1423 productId: 0x00A5 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/142" deviceLabel: LIFX Neon vendorId: 0x1423 productId: 0x008E - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/141" deviceLabel: LIFX Neon vendorId: 0x1423 productId: 0x008D - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/177" deviceLabel: LIFX Ceiling vendorId: 0x1423 productId: 0x00B1 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/170" deviceLabel: LIFX Supercolour (A21) vendorId: 0x1423 productId: 0x00AA - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/205" deviceLabel: LIFX Neon vendorId: 0x1423 productId: 0x00CD - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/265" deviceLabel: Ceiling 13 vendorId: 0x1423 productId: 0x0109 - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/266" deviceLabel: LIFX Ceiling 13 vendorId: 0x1423 productId: 0x010A - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/267" deviceLabel: LIFX Mirror vendorId: 0x1423 productId: 0x010B - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level - id: "5155/268" deviceLabel: LIFX Mirror vendorId: 0x1423 productId: 0x010C - deviceProfileName: light-level-colorTemperature + deviceProfileName: light-color-level #LG - id: "4142/8784" @@ -4251,6 +4256,11 @@ matterGeneric: deviceTypes: - id: 0x0110 # Mounted Dimmable Load Control deviceProfileName: switch-level + - id: "matter/irrigation-system" + deviceLabel: Matter Irrigation System + deviceTypes: + - id: 0x0040 # Irrigation System + deviceProfileName: irrigation-system - id: "matter/water-valve" deviceLabel: Matter Water Valve deviceTypes: diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml index 7f62319984..1f911a9e10 100644 --- a/drivers/SmartThings/matter-switch/profiles/camera.yml +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -134,6 +134,9 @@ deviceConfig: - component: main capability: motionSensor version: 1 + - component: main + capability: videoCapture2 + version: 1 automation: conditions: - component: main diff --git a/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml new file mode 100644 index 0000000000..15896fbfee --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml @@ -0,0 +1,26 @@ +name: irrigation-system +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + optional: true + - id: flowMeasurement + version: 1 + optional: true + - id: operationalState + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Irrigation + diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml index 2f91bcb04e..1b1129e8be 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-fan components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-battery-illuminance-motion-temperature.yml b/drivers/SmartThings/matter-switch/profiles/light-level-battery-illuminance-motion-temperature.yml new file mode 100644 index 0000000000..fb1cd099a4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-battery-illuminance-motion-temperature.yml @@ -0,0 +1,32 @@ +name: light-level-battery-illuminance-motion-temperature +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b1c6a8df8b..e2a04e103a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -8,9 +8,9 @@ local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" local cfg = require "switch_utils.device_configuration" +local button_cfg = cfg.ButtonCfg local device_cfg = cfg.DeviceCfg local switch_cfg = cfg.SwitchCfg -local button_cfg = cfg.ButtonCfg local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" local attribute_handlers = require "switch_handlers.attribute_handlers" @@ -38,8 +38,11 @@ function SwitchLifecycleHandlers.device_added(driver, device) device:send(clusters.OnOff.attributes.OnOff:read(device)) end - -- call device init in case init is not called after added due to device caching - SwitchLifecycleHandlers.device_init(driver, device) + -- The device init event is guaranteed in FW versions 58+, so this is only needed for older hubs + if version.rpc < 10 then + -- call device init in case init is not called after added due to device caching + SwitchLifecycleHandlers.device_init(driver, device) + end end function SwitchLifecycleHandlers.do_configure(driver, device) @@ -47,6 +50,7 @@ function SwitchLifecycleHandlers.do_configure(driver, device) switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + device_cfg.match_child_profile(driver, device) -- because get_parent_device() may cause race conditions if used in init, an initial child subscribe is handled in doConfigure. -- all future calls to subscribe will be handled by the parent device in init device:subscribe() @@ -57,6 +61,7 @@ function SwitchLifecycleHandlers.driver_switched(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then device_cfg.match_profile(driver, device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) @@ -148,6 +153,11 @@ local matter_driver_template = { [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler }, + [clusters.FlowMeasurement.ID] = { + [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_attr_handler, + [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MIN), + [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MAX) + }, [clusters.IlluminanceMeasurement.ID] = { [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, @@ -159,6 +169,11 @@ local matter_driver_template = { [clusters.OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, }, + [clusters.OperationalState.ID] = { + [clusters.OperationalState.attributes.AcceptedCommandList.ID] = attribute_handlers.operational_state_accepted_command_list_attr_handler, + [clusters.OperationalState.attributes.OperationalState.ID] = attribute_handlers.operational_state_attr_handler, + [clusters.OperationalState.attributes.OperationalError.ID] = attribute_handlers.operational_error_attr_handler + }, [clusters.OnOff.ID] = { [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, @@ -226,17 +241,24 @@ local matter_driver_template = { [capabilities.fanSpeedPercent.ID] = { clusters.FanControl.attributes.PercentCurrent }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.operationalState.ID] = { + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower @@ -244,6 +266,9 @@ local matter_driver_template = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, [capabilities.switchLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -287,6 +312,10 @@ local matter_driver_template = { [capabilities.level.ID] = { [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level }, + [capabilities.operationalState.ID] = { + [capabilities.operationalState.commands.pause.NAME] = capability_handlers.handle_operational_state_pause, + [capabilities.operationalState.commands.resume.NAME] = capability_handlers.handle_operational_state_resume + }, [capabilities.statelessColorTemperatureStep.ID] = { [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = capability_handlers.handle_step_color_temperature_by_percent, }, @@ -319,6 +348,7 @@ local matter_driver_template = { capabilities.energyMeter, capabilities.fanMode, capabilities.fanSpeedPercent, + capabilities.flowMeasurement, capabilities.hdr, capabilities.illuminanceMeasurement, capabilities.imageControl, @@ -327,6 +357,7 @@ local matter_driver_template = { capabilities.mechanicalPanTiltZoom, capabilities.motionSensor, capabilities.nightVision, + capabilities.operationalState, capabilities.powerMeter, capabilities.powerConsumptionReport, capabilities.relativeHumidityMeasurement, @@ -345,7 +376,8 @@ local matter_driver_template = { switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") - } + }, + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-switch", matter_driver_template) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua index 4e6b729fa3..ca9dfa76cf 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua @@ -184,7 +184,9 @@ end local function do_configure(driver, device) end -- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end +local function driver_switched(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end local function initial_press_event_handler(driver, device, ib, response) if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index b32b1dd55c..53b35712e7 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -417,7 +417,8 @@ function CameraAttributeHandlers.triggers_handler(driver, device, ib, response) augmentationDuration = trigger.augmentation_duration.value, maxDuration = trigger.max_duration.value, blindDuration = trigger.blind_duration.value, - sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value + sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value or nil }) end device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggers(triggers)) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua index 0a7fcca312..a26afd0ca7 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -259,13 +259,24 @@ end function CameraCapabilityHandlers.handle_remove_zone(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) + local triggers = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME + ) or {} + for _, v in pairs(triggers) do + if v.zoneId == cmd.args.zoneId then + device:send(clusters.ZoneManagement.server.commands.RemoveTrigger(device, endpoint_id, cmd.args.zoneId)) + break + end + end device:send(clusters.ZoneManagement.server.commands.RemoveZone(device, endpoint_id, cmd.args.zoneId)) end function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device, cmd) + local per_zone_sensitivity_supported = camera_utils.feature_supported( + device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY + ) if not cmd.args.augmentationDuration or not cmd.args.maxDuration or not cmd.args.blindDuration or - (camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and - not cmd.args.sensitivity) then + (per_zone_sensitivity_supported and not cmd.args.sensitivity) then local triggers = device:get_latest_state( camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME ) or {} @@ -275,8 +286,7 @@ function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device if not cmd.args.augmentationDuration then cmd.args.augmentationDuration = v.augmentationDuration end if not cmd.args.maxDuration then cmd.args.maxDuration = v.maxDuration end if not cmd.args.blindDuration then cmd.args.blindDuration = v.blindDuration end - if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and - not cmd.args.sensitivity then + if per_zone_sensitivity_supported and not cmd.args.sensitivity then cmd.args.sensitivity = v.sensitivity end found_trigger = true @@ -297,7 +307,7 @@ function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device augmentation_duration = cmd.args.augmentationDuration, max_duration = cmd.args.maxDuration, blind_duration = cmd.args.blindDuration, - sensitivity = cmd.args.sensitivity + sensitivity = per_zone_sensitivity_supported and cmd.args.sensitivity or nil -- omit even if provided by client if per-zone sensitivity is not supported } ) )) @@ -311,7 +321,7 @@ end function CameraCapabilityHandlers.handle_set_sensitivity(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) if not camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then - device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.id)) + device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.sensitivity)) else device.log.warn(string.format("Can't set global zone sensitivity setting, per zone sensitivity enabled.")) end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua index 02b63bb37f..8549b02d18 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua @@ -3,14 +3,23 @@ local camera_fields = require "sub_drivers.camera.camera_utils.fields" local capabilities = require "st.capabilities" -local switch_utils = require "switch_utils.utils" local CameraEventHandlers = {} +local function has_triggered_zone(triggered_zones, zone_id) + for _, zone in ipairs(triggered_zones or {}) do + if zone.zoneId == zone_id then + return true + end + end + return false +end + function CameraEventHandlers.zone_triggered_handler(driver, device, ib, response) local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} - if not switch_utils.tbl_contains(triggered_zones, ib.data.elements.zone.value) then - table.insert(triggered_zones, {zoneId = ib.data.elements.zone.value}) + local zone_id = ib.data.elements.zone.value + if not has_triggered_zone(triggered_zones, zone_id) then + table.insert(triggered_zones, { zoneId = zone_id }) device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) end @@ -18,13 +27,22 @@ end function CameraEventHandlers.zone_stopped_handler(driver, device, ib, response) local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} - for i, v in pairs(triggered_zones) do - if v.zoneId == ib.data.elements.zone.value then - table.remove(triggered_zones, i) - device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) - device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) + local zone_id = ib.data.elements.zone.value + local updated_triggered_zones = {} + local zone_removed = false + + for _, zone in ipairs(triggered_zones) do + if zone.zoneId ~= zone_id then + table.insert(updated_triggered_zones, zone) + else + zone_removed = true end end + + if zone_removed then + device:set_field(camera_fields.TRIGGERED_ZONES, updated_triggered_zones) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(updated_triggered_zones)) + end end return CameraEventHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index a72aa0b234..2aa698a08a 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -43,6 +43,7 @@ function CameraLifecycleHandlers.driver_switched(driver, device) if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then camera_cfg.match_profile(device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function CameraLifecycleHandlers.info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua index 369b93b8de..02bc9e06d2 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua @@ -6,11 +6,12 @@ local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" return function(opts, driver, device) - local EVE_MANUFACTURER_ID = 0x130A - -- this sub driver does NOT support child devices, and ONLY supports Eve devices - -- that do NOT support the Electrical Sensor device type + local EVE_PRIVATE_CLUSTER_ID = 0x130AFC01 + -- this sub driver loads for devices that: + -- 1. Contain the Eve Private Cluster (0x130AFC01) + -- 2. Do NOT have the Standard Electrical Sensor device type if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and + #device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) > 0 and #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) == 0 then return true, require("sub_drivers.eve_energy") end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 8222fc1378..59f0cbb9c2 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -246,7 +246,9 @@ end local function do_configure(driver, device) end -- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end +local function driver_switched(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end local function handle_refresh(self, device) requestData(device) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua index 32b23ccaae..6d6410dffa 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -25,6 +25,7 @@ end function IkeaScrollLifecycleHandlers.driver_switched(driver, device) scroll_cfg.match_profile(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function IkeaScrollLifecycleHandlers.info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua index da9b1c0392..dbad323784 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua @@ -6,28 +6,43 @@ local capabilities = require "st.capabilities" local switch_utils = require "switch_utils.utils" local generic_event_handlers = require "switch_handlers.event_handlers" local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" +local event_utils = require "sub_drivers.ikea_scroll.scroll_utils.event_utils" local IkeaScrollEventHandlers = {} local function rotate_amount_event_helper(device, endpoint_id, num_presses_to_handle) + if num_presses_to_handle <= 0 then return end + -- to cut down on checks, we can assume that if the endpoint is not in ENDPOINTS_UP_SCROLL, it is in ENDPOINTS_DOWN_SCROLL local scroll_direction = switch_utils.tbl_contains(scroll_fields.ENDPOINTS_UP_SCROLL, endpoint_id) and 1 or -1 local scroll_amount = st_utils.clamp_value(scroll_direction * scroll_fields.PER_SCROLL_EVENT_ROTATION * num_presses_to_handle, -100, 100) - device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true})) + + if event_utils.is_valid_scroll_amount(device, scroll_amount) then + device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true})) + end end -- Used by ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL, not ENDPOINTS_PUSH function IkeaScrollEventHandlers.multi_press_ongoing_handler(driver, device, ib, response) if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then - -- Ignore MultiPressOngoing events from push endpoints. device.log.debug("Received MultiPressOngoing event from push endpoint, ignoring.") else - local cur_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.current_number_of_presses_counted.value or 0 - local num_presses_to_handle = cur_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) - if num_presses_to_handle > 0 then - device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, cur_num_presses_counted) - rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + local cur_num_presses_counted = ib.data.elements and ib.data.elements.current_number_of_presses_counted.value or 0 + local cur_multi_press_count = cur_num_presses_counted + if #response.info_blocks > 1 then + -- note: keep in mind that response blocks with mutliple info blocks are not supported by unit tests today. + if event_utils.is_last_valid_info_block(ib.event_id, cur_num_presses_counted, response.info_blocks) then + local aggregated_presses = event_utils.aggregate_scroll_amount_for_info_blocks(device, response.info_blocks) or {} + cur_num_presses_counted = aggregated_presses.total_presses or 0 + cur_multi_press_count = aggregated_presses.presses_in_current_chain or 0 + else + device.log.debug("Received MultiPressOngoing event that is not the last valid info block, ignoring.") + return + end end + local num_presses_to_handle = cur_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_HANDLED) or 0) + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_HANDLED, cur_multi_press_count) + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) end end @@ -35,13 +50,21 @@ function IkeaScrollEventHandlers.multi_press_complete_handler(driver, device, ib if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then generic_event_handlers.multi_press_complete_handler(driver, device, ib, response) else - local total_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.total_number_of_presses_counted.value or 0 - local num_presses_to_handle = total_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) - if num_presses_to_handle > 0 then - rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + local total_num_presses_counted = ib.data.elements and ib.data.elements.total_number_of_presses_counted.value or 0 + if #response.info_blocks > 1 then + -- note: keep in mind that response blocks with mutliple info blocks are not supported by unit tests today. + if event_utils.is_last_valid_info_block(ib.event_id, total_num_presses_counted, response.info_blocks) then + local aggregated_presses = event_utils.aggregate_scroll_amount_for_info_blocks(device, response.info_blocks) or {} + total_num_presses_counted = aggregated_presses.total_presses or 0 + else + device.log.debug("Received MultiPressComplete event that is not the last valid info block, ignoring.") + return + end end - -- reset the LATEST_NUMBER_OF_PRESSES_COUNTED to nil at the end of a MultiPress chain. - device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, nil) + local num_presses_to_handle = total_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_HANDLED) or 0) + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + -- always reset the LATEST_NUMBER_OF_PRESSES_HANDLED to nil at the end of a handled MultiPress chain. + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_HANDLED, nil) end end @@ -49,12 +72,7 @@ function IkeaScrollEventHandlers.initial_press_handler(driver, device, ib, respo if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then generic_event_handlers.initial_press_handler(driver, device, ib, response) else - -- the magic number "1" occurs in this handler since the InitialPress event represents the first press. - local latest_presses_counted = device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0 - if latest_presses_counted == 0 then - device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, 1) - rotate_amount_event_helper(device, ib.endpoint_id, 1) - end + device.log.debug("Received InitialPress event from scroll endpoint, ignoring.") end end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua index 457804d495..0ad8a68826 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua @@ -22,7 +22,6 @@ function IkeaScrollConfiguration.configure_buttons(device) for _, ep in ipairs(scroll_fields.ENDPOINTS_PUSH) do device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) end for _, ep in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do -- and by extension, ENDPOINTS_DOWN_SCROLL device:emit_event_for_endpoint(ep, capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}})) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua new file mode 100644 index 0000000000..8a7a684cf6 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua @@ -0,0 +1,76 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollEventUtils = {} + + +function IkeaScrollEventUtils.requeue_clear_scroll_state(device) + -- cancel any previously queued clear state actions to prevent unintended clears + if device:get_field(scroll_fields.CLEAR_STATE_TIMER) then + device.thread:cancel_timer(device:get_field(scroll_fields.CLEAR_STATE_TIMER)) + end + local new_timer = device.thread:call_with_delay(scroll_fields.CLEAR_STATE_DELAY_S, function() + device:set_field(scroll_fields.GLOBAL_ROTATE_AMOUNT_STATE, 0) + end) + device:set_field(scroll_fields.CLEAR_STATE_TIMER, new_timer) +end + +function IkeaScrollEventUtils.is_valid_scroll_amount(device, scroll_amount) + local global_rotate_amount_state = device:get_field(scroll_fields.GLOBAL_ROTATE_AMOUNT_STATE) or 0 + local is_rotate_amount_state_at_bounds = (scroll_amount < 0 and global_rotate_amount_state <= -100) or (scroll_amount > 0 and global_rotate_amount_state >= 100) + if is_rotate_amount_state_at_bounds then + return false + end + + device:set_field(scroll_fields.GLOBAL_ROTATE_AMOUNT_STATE, st_utils.clamp_value(global_rotate_amount_state + scroll_amount, -100, 100)) + IkeaScrollEventUtils.requeue_clear_scroll_state(device) + return true +end + +-- inspect all info blocks to find the last one that is not an InitialPress event. We will +-- only try to emit a rotateAmount event if the current info block being handled is that last one. +function IkeaScrollEventUtils.is_last_valid_info_block(cur_info_block_event_id, cur_info_block_value, info_blocks) + local last_valid_emission_idx = #info_blocks + -- Ignore all InitialPress events in a multi-response block + while (last_valid_emission_idx > 1) and (info_blocks[last_valid_emission_idx].info_block.event_id == clusters.Switch.events.InitialPress.ID) do + last_valid_emission_idx = last_valid_emission_idx - 1 + end + + -- Because the info block does not include the unique_key Matter defined event number, this + -- logic is a best guess at matching the current info block to the last valid info block. + local emission_ib = info_blocks[last_valid_emission_idx].info_block + if emission_ib.event_id ~= cur_info_block_event_id then + return false + elseif emission_ib.event_id == clusters.Switch.events.MultiPressComplete.ID then + local last_valid_ib_value = emission_ib.data.elements and emission_ib.data.elements.total_number_of_presses_counted.value or 0 + return last_valid_ib_value == cur_info_block_value + elseif emission_ib.event_id == clusters.Switch.events.MultiPressOngoing.ID then + local last_valid_ib_value = emission_ib.data.elements and emission_ib.data.elements.current_number_of_presses_counted.value or 0 + return last_valid_ib_value == cur_info_block_value + elseif last_valid_emission_idx == 1 then -- aka, all ib's are InitialPress + return true + end + + return false +end + +function IkeaScrollEventUtils.aggregate_scroll_amount_for_info_blocks(device, info_blocks) + local total_presses = 0 + local presses_in_current_chain = 0 + for _, ib in ipairs(info_blocks) do + if ib.info_block.event_id == clusters.Switch.events.MultiPressOngoing.ID then + presses_in_current_chain = ib.info_block.data.elements and ib.info_block.data.elements.current_number_of_presses_counted.value or 0 + elseif ib.info_block.event_id == clusters.Switch.events.MultiPressComplete.ID then + total_presses = total_presses + (ib.info_block.data.elements and ib.info_block.data.elements.total_number_of_presses_counted.value or 0) + presses_in_current_chain = 0 + end + end + total_presses = total_presses + presses_in_current_chain -- aggregate any presses to the total from the current chain + return { total_presses = total_presses, presses_in_current_chain = presses_in_current_chain } +end + +return IkeaScrollEventUtils diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua index 5e98a829c0..27e0dbcd6d 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua @@ -1,7 +1,6 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local IkeaScrollFields = {} @@ -18,14 +17,21 @@ IkeaScrollFields.ENDPOINTS_UP_SCROLL = {1, 4, 7} -- Generic Switch Endpoints used for Down Scroll functionality IkeaScrollFields.ENDPOINTS_DOWN_SCROLL = {2, 5, 8} --- Maximum number of presses at a time -IkeaScrollFields.MAX_SCROLL_PRESSES = 18 - -- Amount to rotate per scroll event -IkeaScrollFields.PER_SCROLL_EVENT_ROTATION = st_utils.round(1 / IkeaScrollFields.MAX_SCROLL_PRESSES * 100) +-- 6 == st_utils.round(1/18 * 100), where 18 is the maximum number of presses that can be pressed at a time +IkeaScrollFields.PER_SCROLL_EVENT_ROTATION = 6 + +-- Field to track the latest number of presses handled during a single scroll event sequence +IkeaScrollFields.LATEST_NUMBER_OF_PRESSES_HANDLED = "__latest_number_of_presses_handled" + +-- Field to track the global rotate amount state for the device to ensure no scroll events mapped outside of state bounds are emitted +IkeaScrollFields.GLOBAL_ROTATE_AMOUNT_STATE = "__global_rotate_amount_state" + +-- Stores a timer object, which is required to cancel a timer early +IkeaScrollFields.CLEAR_STATE_TIMER = "__clear_state_timer" --- Field to track the latest number of presses counted during a single scroll event sequence -IkeaScrollFields.LATEST_NUMBER_OF_PRESSES_COUNTED = "__latest_number_of_presses_counted" +-- Delay in seconds to wait before clearing the global rotate amount state after the last scroll event +IkeaScrollFields.CLEAR_STATE_DELAY_S = 8 -- Required Events for the ENDPOINTS_PUSH. IkeaScrollFields.switch_press_subscribed_events = { diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua index 9a9f95228b..5e249955e2 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua @@ -4,6 +4,7 @@ local im = require "st.matter.interaction_model" local clusters = require "st.matter.clusters" local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" +local switch_fields = require "switch_utils.fields" local IkeaScrollUtils = {} @@ -28,11 +29,18 @@ function IkeaScrollUtils.subscribe(device) subscribe_request:with_info_block(ib) end end + local cluster_id = clusters.PowerSource.ID + local attr_id = clusters.PowerSource.attributes.BatPercentRemaining.ID local ib = im.InteractionInfoBlock( - scroll_fields.ENDPOINT_POWER_SOURCE, clusters.PowerSource.ID, clusters.PowerSource.attributes.BatPercentRemaining.ID + scroll_fields.ENDPOINT_POWER_SOURCE, cluster_id, attr_id ) subscribe_request:with_info_block(ib) device:send(subscribe_request) + + local subscribed_attrs = device:get_field(switch_fields.SUBSCRIBED_ATTRIBUTES_KEY) or {} + subscribed_attrs[cluster_id] = subscribed_attrs[cluster_id] or {} + subscribed_attrs[cluster_id][attr_id] = ib + device:set_field(switch_fields.SUBSCRIBED_ATTRIBUTES_KEY, subscribed_attrs) end return IkeaScrollUtils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua index 1572886089..9c80c247be 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua @@ -36,7 +36,6 @@ local function configure_buttons(device) device.log.info(string.format("Configuring Supported Values for generic switch endpoint %d", ep)) local supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) device:emit_event_for_endpoint(ep, supportedButtonValues_event) - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) else device.log.info(string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) end diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 20bd92881e..45694bc613 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -106,6 +106,9 @@ function AttributeHandlers.current_saturation_handler(driver, device, ib, respon end function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, response) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("colorTemperature", "colorTemperature") + end local temp_in_mired = ib.data.value if temp_in_mired == nil then return @@ -128,12 +131,12 @@ function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, if device:get_field(fields.IS_PARENT_CHILD_DEVICE) == true then temp_device = switch_utils.find_child(device, ib.endpoint_id) or device end - local most_recent_temp = temp_device:get_field(fields.MOST_RECENT_TEMP) + local latest_requested_kelvin = temp_device:get_field(fields.LATEST_REQUESTED_KELVIN) -- this is to avoid rounding errors from the round-trip conversion of Kelvin to mireds - if most_recent_temp ~= nil and - most_recent_temp <= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and - most_recent_temp >= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then - temp = most_recent_temp + if latest_requested_kelvin and + latest_requested_kelvin <= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and + latest_requested_kelvin >= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then + temp = latest_requested_kelvin end device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperature(temp)) end @@ -550,4 +553,79 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) end + +-- [[ OPERATIONAL STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.operational_state_accepted_command_list_attr_handler(driver, device, ib, response) + if ib.data.elements == nil then return end + local accepted_command_list = {} + for _, accepted_command in ipairs(ib.data.elements) do + local accepted_command_id = accepted_command.value + if fields.operational_state_command_map[accepted_command_id] ~= nil then + table.insert(accepted_command_list, fields.operational_state_command_map[accepted_command_id]) + end + end + local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.operational_state_attr_handler(driver, device, ib, response) + if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused()) + end +end + +function AttributeHandlers.operational_error_attr_handler(driver, device, ib, response) + if ib.data.elements == nil or ib.data.elements.error_state_id == nil or ib.data.elements.error_state_id.value == nil then return end + if version.api < 10 then + clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data) + end + local operationalError = ib.data.elements.error_state_id.value + if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + end +end + + +-- [[ FLOW MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.flow_attr_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local flow = measured_value / 10.0 + local unit = "m^3/h" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) + end +end + +function AttributeHandlers.flow_attr_handler_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local flow_bound = ib.data.value / 10.0 + local unit = "m^3/h" + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) + local min = switch_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) + end + end + end +end + return AttributeHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index eec323a335..573d2b791b 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -50,11 +50,15 @@ end -- [[ STATELESS SWITCH LEVEL STEP CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_step_level(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254) if step_size == 0 then return end local endpoint_id = device:component_to_endpoint(cmd.component) local step_mode = step_size > 0 and clusters.LevelControl.types.StepMode.UP or clusters.LevelControl.types.StepMode.DOWN - device:send(clusters.LevelControl.server.commands.Step(device, endpoint_id, step_mode, math.abs(step_size), fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) + local transition_time = device:get_field(fields.TRANSITION_TIME.SWITCH_LEVEL_STEP) or fields.DEFAULT_STEP_TRANSITION_TIME + device:send(clusters.LevelControl.server.commands.Step(device, endpoint_id, step_mode, math.abs(step_size), transition_time, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) end @@ -67,10 +71,10 @@ function CapabilityHandlers.handle_set_color(driver, device, cmd) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.color.hue) local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) else local x, y, _ = st_utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) end device:send(req) end @@ -80,7 +84,7 @@ function CapabilityHandlers.handle_set_hue(driver, device, cmd) local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.hue) - local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) device:send(req) else device.log.warn("Device does not support huesat features on its color control cluster") @@ -92,7 +96,7 @@ function CapabilityHandlers.handle_set_saturation(driver, device, cmd) local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.saturation) - local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) device:send(req) else device.log.warn("Device does not support huesat features on its color control cluster") @@ -103,19 +107,24 @@ end -- [[ COLOR TEMPERATURE CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_set_color_temperature(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end local endpoint_id = device:component_to_endpoint(cmd.component) local temp_in_kelvin = cmd.args.temperature - local min_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, endpoint_id) - local max_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, endpoint_id) + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_temp_kelvin = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, endpoint_id) + local max_temp_kelvin = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, endpoint_id) local temp_in_mired = st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_kelvin) if min_temp_kelvin ~= nil and temp_in_kelvin <= min_temp_kelvin then - temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) + temp_in_mired = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then - temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) + temp_in_mired = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) end - local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) - device:set_field(fields.MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) + local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + device:set_field(fields.LATEST_REQUESTED_KELVIN, cmd.args.temperature) device:send(req) end @@ -123,19 +132,29 @@ end -- [[ STATELESS COLOR TEMPERATURE STEP CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_step_color_temperature_by_percent(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end local step_percent_change = cmd.args and cmd.args.stepSize or 0 if step_percent_change == 0 then return end step_percent_change = st_utils.clamp_value(step_percent_change, -100, 100) local endpoint_id = device:component_to_endpoint(cmd.component) -- before the Matter 1.3 lua libs update (HUB FW 55), there was no ColorControl StepModeEnum type defined local step_mode = step_percent_change > 0 and (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.DOWN or 3) or (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.UP or 1) - local min_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) or fields.DEFAULT_MIRED_MIN - local max_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) or fields.DEFAULT_MIRED_MAX + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_mireds = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) + local max_mireds = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) + -- since colorTemperatureRange is only set after both custom bounds are, use defaults if any custom bound is missing + if not (min_mireds and max_mireds) then + min_mireds = fields.DEFAULT_MIRED_MIN + max_mireds = fields.DEFAULT_MIRED_MAX + end local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0)) - device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, endpoint_id, step_mode, step_size_in_mireds, fields.TRANSITION_TIME_FAST, min_mireds, max_mireds, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) + local transition_time = device:get_field(fields.TRANSITION_TIME.COLOR_TEMP_STEP) or fields.DEFAULT_STEP_TRANSITION_TIME + device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, endpoint_id, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) end - -- [[ VALVE CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_valve_open(driver, device, cmd) @@ -216,4 +235,21 @@ function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) end end + +-- [[ OPERATIONAL STATE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_operational_state_resume(driver, device, cmd) + local endpoint_id = device:get_endpoints(clusters.OperationalState.ID)[1] + device:send(clusters.OperationalState.server.commands.Resume(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + +function CapabilityHandlers.handle_operational_state_pause(driver, device, cmd) + local endpoint_id = device:get_endpoints(clusters.OperationalState.ID)[1] + device:send(clusters.OperationalState.server.commands.Pause(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + return CapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index ab2e075eb7..772669ebd7 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -20,17 +20,18 @@ local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} +local ValveDeviceConfiguration = {} function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created - return + return end table.sort(server_cluster_ep_ids) for device_num, ep_id in ipairs(server_cluster_ep_ids) do if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint local label_and_name = string.format("%s %d", device.label, device_num) - local child_profile, _ = assign_profile_fn(device, ep_id, true) + local child_profile, optional_component_capabilities = assign_profile_fn(device, ep_id, true) local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) if not existing_child_device then driver:try_create_device({ @@ -43,7 +44,8 @@ function ChildConfiguration.create_or_update_child_devices(driver, device, serve }) else existing_child_device:try_update_metadata({ - profile = child_profile + profile = child_profile, + optional_component_capabilities = optional_component_capabilities }) end end @@ -74,7 +76,6 @@ function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_ return "fan-modular", optional_supported_component_capabilities end - function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) @@ -183,16 +184,62 @@ function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep if supportedButtonValues_event then device:emit_event_for_endpoint(ep, supportedButtonValues_event) end - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) else device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) end end end +function ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep(device, irrigation_system_ep_id, is_child_device) + local main_component_capabilities = {} + local profile_name = "irrigation-system" + + local valve_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + table.sort(valve_ep_ids) + local supports_level = switch_utils.find_cluster_on_ep( + switch_utils.get_endpoint_info(device, is_child_device and irrigation_system_ep_id or valve_ep_ids[1]), + clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL} + ) + if supports_level then + table.insert(main_component_capabilities, capabilities.level.ID) + end + + if is_child_device then + return profile_name, {{"main", main_component_capabilities}} + end + + local irrigation_system_ep_info = switch_utils.get_endpoint_info(device, irrigation_system_ep_id) + if switch_utils.find_cluster_on_ep(irrigation_system_ep_info, clusters.FlowMeasurement.ID) then + table.insert(main_component_capabilities, capabilities.flowMeasurement.ID) + end + if switch_utils.find_cluster_on_ep(irrigation_system_ep_info, clusters.OperationalState.ID) then + table.insert(main_component_capabilities, capabilities.operationalState.ID) + end + + return profile_name, {{"main", main_component_capabilities}} +end + -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- +function DeviceConfiguration.match_child_profile(driver, device) + local parent_device = device:get_parent_device() + local irrigation_system_ep_ids = switch_utils.get_endpoints_by_device_type( + parent_device, + fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM + ) + if #irrigation_system_ep_ids > 0 then + ChildConfiguration.create_or_update_child_devices( + driver, + parent_device, + {device:get_endpoint()}, + nil, + ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep + ) + end +end + local function profiling_data_still_required(device) for _, field in pairs(fields.profiling_data) do if device:get_field(field) == nil then @@ -209,14 +256,6 @@ function DeviceConfiguration.match_profile(driver, device) local optional_component_capabilities local updated_profile - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then - updated_profile = "water-valve" - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, - {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - updated_profile = updated_profile .. "-level" - end - end - local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) @@ -226,7 +265,7 @@ function DeviceConfiguration.match_profile(driver, device) updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end if generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then - updated_profile = "light-level-motion" + updated_profile = switch_utils.get_product_override_field(device, "target_profile") or "light-level-motion" elseif switch_utils.check_switch_category_vendor_overrides(device) then -- check whether the overwrite should be over "plug" or "light" based on the current profile local overwrite_category = string.find(updated_profile, "plug") and "plug" or "light" @@ -238,8 +277,20 @@ function DeviceConfiguration.match_profile(driver, device) end end - local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) - if #fan_device_type_ep_ids > 0 then + local irrigation_system_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM) + local valve_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + if #irrigation_system_ep_ids > 0 then + updated_profile, optional_component_capabilities = ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep(device, irrigation_system_ep_ids[1], false) + ChildConfiguration.create_or_update_child_devices(driver, device, valve_ep_ids, default_endpoint_id, ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep) + elseif #valve_ep_ids > 0 then + updated_profile = "water-valve" + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then + updated_profile = updated_profile .. "-level" + end + end + + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) > 0 then updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) end @@ -256,7 +307,9 @@ function DeviceConfiguration.match_profile(driver, device) end return { + ButtonCfg = ButtonDeviceConfiguration, + ChildCfg = ChildConfiguration, DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, - ButtonCfg = ButtonDeviceConfiguration + ValveCfg = ValveDeviceConfiguration } diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 1432b18c74..02c864fa4f 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,11 +1,11 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" local SwitchFields = {} -SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" +SwitchFields.LATEST_REQUESTED_KELVIN = "mostRecentTemp" SwitchFields.RECEIVED_X = "receivedX" SwitchFields.RECEIVED_Y = "receivedY" SwitchFields.HUESAT_SUPPORT = "huesatSupport" @@ -13,16 +13,12 @@ SwitchFields.HUESAT_SUPPORT = "huesatSupport" SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 -- These values are a "sanity check" to check that values we are getting are reasonable -local COLOR_TEMPERATURE_KELVIN_MAX = 15000 -local COLOR_TEMPERATURE_KELVIN_MIN = 1000 -SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) -- 1000 Mireds -SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) -- 67 Mireds +SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = 67 -- 15000 Kelvin +SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = 1000 -- 1000 Kelvin -- These values are the config bounds in the default Matter profiles (e.g. light-level-colorTemperature, light-color-level) -local DEFAULT_KELVIN_MIN = 2200 -local DEFAULT_KELVIN_MAX = 6500 -SwitchFields.DEFAULT_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/DEFAULT_KELVIN_MAX) -- 154 Mireds -SwitchFields.DEFAULT_MIRED_MAX = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/DEFAULT_KELVIN_MIN) -- 455 Mireds +SwitchFields.DEFAULT_MIRED_MIN = 154 -- 6500 Kelvin +SwitchFields.DEFAULT_MIRED_MAX = 455 -- 2200 Kelvin SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 @@ -38,6 +34,7 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, + IRRIGATION_SYSTEM = 0x0040, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, @@ -52,6 +49,7 @@ SwitchFields.DEVICE_TYPE_ID = { DIMMER = 0x0104, COLOR_DIMMER = 0x0105, }, + WATER_VALVE = 0x0042, } SwitchFields.device_type_profile_map = { @@ -90,6 +88,9 @@ SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received" SwitchFields.LEVEL_MIN = "__level_min" SwitchFields.LEVEL_MAX = "__level_max" SwitchFields.COLOR_MODE = "__color_mode" +SwitchFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" +SwitchFields.FLOW_MIN = "__flow_min" +SwitchFields.FLOW_MAX = "__flow_max" SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" @@ -98,6 +99,8 @@ SwitchFields.updated_fields = { { current_field_name = "__switch_intialized", updated_field_name = nil }, { current_field_name = "__energy_management_endpoint", updated_field_name = nil }, { current_field_name = "__total_imported_energy", updated_field_name = nil }, + { current_field_name = "__last_imported_report_timestamp", updated_field_name = nil }, + { current_field_name = "mostRecentTemp", updated_field_name = nil }, } SwitchFields.vendor_overrides = { @@ -117,6 +120,9 @@ SwitchFields.vendor_overrides = { [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, + [0x1209] = { -- Bosch + [0x3013] = {target_profile = "light-level-battery-illuminance-motion-temperature"} + } } SwitchFields.switch_category_vendor_overrides = { @@ -146,6 +152,11 @@ SwitchFields.switch_category_vendor_overrides = { {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} } +SwitchFields.operational_state_command_map = { + [clusters.OperationalState.commands.Pause.ID] = "pause", + [clusters.OperationalState.commands.Resume.ID] = "resume" +} + --- stores a table of endpoints that support the Electrical Sensor device type, used during profiling --- in AvailableEndpoints and PartsList handlers for SET and TREE PowerTopology features, respectively SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" @@ -193,8 +204,13 @@ SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" SwitchFields.TEMP_MIN = "__temp_min" SwitchFields.TEMP_MAX = "__temp_max" -SwitchFields.TRANSITION_TIME = 0 -- number of 10ths of a second -SwitchFields.TRANSITION_TIME_FAST = 3 -- 0.3 seconds +SwitchFields.ZERO_TRANSITION_TIME = 0 -- 0.0 seconds +SwitchFields.DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds, measured in tenths of a second as per the Matter spec + +SwitchFields.TRANSITION_TIME = { + SWITCH_LEVEL_STEP = "__switch_level_step_transition_time", + COLOR_TEMP_STEP = "__color_temp_step_transition_time", +} -- For Level/Color Control cluster commands, this field indicates which bits in the OptionsOverride field are valid. In this case, we specify that the ExecuteIfOff option (bit 1) may be overridden. SwitchFields.OPTIONS_MASK = 0x01 diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index f949a15a56..d2457bfb4d 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -151,10 +151,6 @@ function utils.find_default_endpoint(device) return device.MATTER_DEFAULT_ENDPOINT end - local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) - local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) - local get_first_non_zero_endpoint = function(endpoints) table.sort(endpoints) for _,ep in ipairs(endpoints) do @@ -166,23 +162,24 @@ function utils.find_default_endpoint(device) end -- Return the first fan endpoint as the default endpoint if any is found + local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) if #fan_endpoint_ids > 0 then return get_first_non_zero_endpoint(fan_endpoint_ids) end - -- Return the first onoff endpoint as the default endpoint if no momentary switch endpoints are present - if #momentary_switch_ep_ids == 0 and #onoff_ep_ids > 0 then - return get_first_non_zero_endpoint(onoff_ep_ids) - end - - -- Return the first momentary switch endpoint as the default endpoint if no onoff endpoints are present - if #onoff_ep_ids == 0 and #momentary_switch_ep_ids > 0 then - return get_first_non_zero_endpoint(momentary_switch_ep_ids) + -- Return the first water valve endpoint as the default endpoint if any is found + local water_valve_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + if #water_valve_endpoint_ids > 0 then + return get_first_non_zero_endpoint(water_valve_endpoint_ids) end -- If both onoff and momentary switch endpoints are present, check the device type on the first onoff -- endpoint. If it is not a supported device type, return the first momentary switch endpoint as the - -- default endpoint. + -- default endpoint. Else return the first onoff endpoint as the default endpoint. + -- + -- If only one of the two types of endpoints are present, return the first endpoint of the present type. + local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if #onoff_ep_ids > 0 and #momentary_switch_ep_ids > 0 then local default_endpoint_id = get_first_non_zero_endpoint(onoff_ep_ids) if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then @@ -191,6 +188,10 @@ function utils.find_default_endpoint(device) device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") return get_first_non_zero_endpoint(momentary_switch_ep_ids) end + elseif #onoff_ep_ids > 0 then + return get_first_non_zero_endpoint(onoff_ep_ids) + elseif #momentary_switch_ep_ids > 0 then + return get_first_non_zero_endpoint(momentary_switch_ep_ids) end device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) @@ -391,22 +392,23 @@ end function utils.report_power_consumption_to_st_energy(device, endpoint_id, total_imported_energy_wh) local current_time = os.time() - local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + local last_time = utils.get_field_for_endpoint(device, fields.LAST_IMPORTED_REPORT_TIMESTAMP, endpoint_id) or 0 -- Ensure that the previous report was sent at least 15 minutes ago if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + utils.set_field_for_endpoint(device, fields.LAST_IMPORTED_REPORT_TIMESTAMP, endpoint_id, current_time, { persist = true }) local previous_imported_report = utils.get_latest_state_for_endpoint(device, endpoint_id, capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = total_imported_energy_wh }) -- default value if nil + local delta_energy = total_imported_energy_wh - previous_imported_report.energy -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC device:emit_event_for_endpoint(endpoint_id, capabilities.powerConsumptionReport.powerConsumption({ start = epoch_to_iso8601(last_time), ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = total_imported_energy_wh - previous_imported_report.energy, + deltaEnergy = delta_energy >= 0.0 and delta_energy or total_imported_energy_wh, -- clarifying assumption: a negative delta means the meter was reset energy = total_imported_energy_wh })) end @@ -429,7 +431,7 @@ function utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sens table.sort(associated_endpoint_ids) local primary_associated_ep_id = associated_endpoint_ids[1] -- map the required electrical tags for this electrical sensor EP with the first associated EP ID, used later during profling. - utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags) + utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags, {persist = true}) utils.set_field_for_endpoint(device, fields.ASSIGNED_CHILD_KEY, electrical_sensor_ep.endpoint_id, string.format("%d", primary_associated_ep_id), { persist = true }) return true end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 305c7e682e..50713626f2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -97,13 +97,10 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ local function configure_buttons() test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false}))) end local function test_init() @@ -130,7 +127,6 @@ local function test_init() end test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index 667e68ec16..f763cc769f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -7,7 +7,6 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -local button_attr = capabilities.button.button local version = require "version" if version.api < 11 then @@ -147,16 +146,9 @@ local cumulative_report_val_39 = { local function configure_buttons() test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) end local function test_init() @@ -181,15 +173,12 @@ local function test_init() -- Test added -> doConfigure logic test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) configure_buttons() aqara_mock_device:expect_metadata_update({ profile = "4-button" }) aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - -- to test powerConsumptionReport - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") for _, child in pairs(aqara_mock_children) do test.mock_device.add_test_device(child) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 180604431b..dae6b09a6c 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -181,8 +181,6 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) end test.set_test_init_function(test_init) @@ -195,79 +193,9 @@ local function test_init_periodic() subscribe_request:merge(cluster:subscribe(mock_device_periodic)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) - test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end -test.register_message_test( - "On command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 2) - } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Off command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "off", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 2) - } - } - }, - { - min_api_version = 17 - } -) - test.register_message_test( "Active power measurement should generate correct messages", { @@ -550,8 +478,14 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) ) - -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the parent). - + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) test.wait_for_events() test.mock_time.advance_time(1500) @@ -586,7 +520,7 @@ test.register_coroutine_test( mock_child:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ start = "1970-01-01T00:15:01Z", ["end"] = "1970-01-01T00:40:00Z", - deltaEnergy = 0.0, + deltaEnergy = 1.0, energy = 20.0 })) ) @@ -603,7 +537,14 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) ) - -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the child). + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 1.0, + energy = 20.0 + })) + ) end, { test_init = test_init, @@ -671,7 +612,7 @@ test.register_coroutine_test( mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ start = "1970-01-01T00:15:01Z", ["end"] = "1970-01-01T00:48:20Z", - deltaEnergy = -4.0, + deltaEnergy = 19.0, energy = 19.0 })) ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua index f618389ba6..d691f2af27 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -128,78 +128,10 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) end test.set_test_init_function(test_init) -test.register_message_test( - "On command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 2) - } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Off command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "off", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 2) - } - } - }, - { - min_api_version = 17 - } -) - test.register_message_test( "Active power measurement should generate correct messages", { diff --git a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua index 5fa58ce8d3..e90f5a70da 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -15,6 +15,28 @@ local PRIVATE_ATTR_ID_WATT = 0x130A000A local PRIVATE_ATTR_ID_WATT_ACCUMULATED = 0x130A000B local PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT = 0x130A000E +-- Helper function to add get_endpoints method to mock devices +local function add_get_endpoints_to_mock(device) + device.get_endpoints = function(self, cluster_id, opts) + opts = opts or {} + local eps = {} + for _, ep in ipairs(self.endpoints) do + for _, cluster in ipairs(ep.clusters or {}) do + if cluster.cluster_id == cluster_id then + -- Check feature_bitmap if specified + if opts.feature_bitmap == nil or + (cluster.feature_map and (cluster.feature_map & opts.feature_bitmap) == opts.feature_bitmap) then + table.insert(eps, ep.endpoint_id) + break + end + end + end + end + return eps + end + return device +end + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("power-energy-powerConsumption.yml"), manufacturer_info = { @@ -53,8 +75,9 @@ local mock_device = test.mock_device.build_test_matter_device({ } } }) +add_get_endpoints_to_mock(mock_device) -local mock_device_electrical_sensor = test.mock_device.build_test_matter_device({ +local mock_eve_device_using_electrical_sensor = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("plug-energy-powerConsumption.yml"), manufacturer_info = { vendor_id = 0x130A, @@ -112,30 +135,42 @@ local mock_device_electrical_sensor = test.mock_device.build_test_matter_device( } } }) +add_get_endpoints_to_mock(mock_eve_device_using_electrical_sensor) -local function test_init_electrical_sensor() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device_electrical_sensor) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +-- Mock device without Eve Private Cluster (should not match eve_energy sub-driver) +local mock_device_without_private_cluster = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-binary.yml"), + manufacturer_info = { + vendor_id = 0x130A, + product_id = 0x0051, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, --u32 bitmap + } + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 } -- On/Off Plug + } + } } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_electrical_sensor) - for i, clus in ipairs(cluster_subscribe_list) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_electrical_sensor)) end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "added" }) - test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "init" }) - test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "doConfigure" }) - mock_device_electrical_sensor:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) - mock_device_electrical_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end +}) +add_get_endpoints_to_mock(mock_device_without_private_cluster) local function test_init() local cluster_subscribe_list = { @@ -153,6 +188,31 @@ local function test_init() end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Eve Energy sub-driver can_handle should return true for devices with Eve Private Cluster and no Electrical Sensor", + function() + local eve_energy_can_handle = require("sub_drivers.eve_energy.can_handle") + local result, sub_driver = eve_energy_can_handle(nil, nil, mock_device) + assert(result == true, "can_handle should return true for Eve device with private cluster and no electrical sensor") + assert(sub_driver ~= nil, "sub_driver should be returned") + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Eve Energy sub-driver can_handle should return false for devices without Eve Private Cluster", + function() + local eve_energy_can_handle = require("sub_drivers.eve_energy.can_handle") + local result = eve_energy_can_handle(nil, nil, mock_device_without_private_cluster) + assert(result == false, "can_handle should return false for device without Eve Private Cluster") + end, + { + min_api_version = 17 + } +) + test.register_message_test( "On command should send the appropriate commands", { @@ -523,7 +583,7 @@ local cumulative_report_val_39 = { test.register_coroutine_test( "Cumulative Energy measurement should generate correct messages", function() - local mock_device = mock_device_electrical_sensor + mock_device = mock_eve_device_using_electrical_sensor test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive( @@ -579,7 +639,10 @@ test.register_coroutine_test( ) end, { - test_init = test_init_electrical_sensor, + test_init = function() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_eve_device_using_electrical_sensor) + end, min_api_version = 17 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua index e07edf6fc9..f421b17f79 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -156,13 +156,9 @@ local function ikea_scroll_subscribe() end local function expect_configure_buttons() - local button_attr = capabilities.button.button test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 3)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", button_attr.pushed({state_change = false}))) test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 6)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", button_attr.pushed({state_change = false}))) test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 9)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", button_attr.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) @@ -253,12 +249,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -273,7 +264,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(6, {state_change = true})) + capabilities.knob.rotateAmount(12, {state_change = true})) }, { channel = "matter", @@ -311,12 +302,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -331,7 +317,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(6, {state_change = true})) + capabilities.knob.rotateAmount(12, {state_change = true})) }, { channel = "matter", @@ -377,12 +363,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -397,7 +378,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-6, {state_change = true})) + capabilities.knob.rotateAmount(-12, {state_change = true})) }, { channel = "matter", @@ -435,12 +416,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -455,7 +431,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-6, {state_change = true})) + capabilities.knob.rotateAmount(-12, {state_change = true})) }, { channel = "matter", @@ -501,12 +477,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -521,7 +492,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(6, {state_change = true})) + capabilities.knob.rotateAmount(12, {state_change = true})) }, { channel = "matter", @@ -557,12 +528,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(-6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -577,7 +543,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(-6, {state_change = true})) + capabilities.knob.rotateAmount(-12, {state_change = true})) }, { channel = "matter", @@ -613,12 +579,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -633,7 +594,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(6, {state_change = true})) + capabilities.knob.rotateAmount(12, {state_change = true})) }, { channel = "matter", @@ -669,12 +630,7 @@ test.register_message_test( ) }, }, - { - channel = "capability", - direction = "send", - message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(-6, {state_change = true})) - }, + -- ignore InitialPress events during scroll { channel = "matter", direction = "receive", @@ -689,7 +645,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(-6, {state_change = true})) + capabilities.knob.rotateAmount(-12, {state_change = true})) }, { channel = "matter", @@ -781,4 +737,16 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.capability:__queue_receive( + {mock_ikea_scroll.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + ) + local read_request = clusters.PowerSource.attributes.BatPercentRemaining:read(mock_ikea_scroll, 0) + test.socket.matter:__expect_send({mock_ikea_scroll.id, read_request}) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua index ccf952a824..b22498eaf7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua @@ -4,12 +4,8 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local st_utils = require "st.utils" local clusters = require "st.matter.clusters" -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("light-color-level-illuminance-motion.yml"), @@ -40,7 +36,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { - {device_type_id = 0x010C, device_type_revision = 1} -- ColorTemperatureLight + {device_type_id = 0x010D, device_type_revision = 1} -- Extended Color Light } }, { @@ -64,326 +60,75 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) -local function set_color_mode(device, endpoint, color_mode) - test.socket.matter:__queue_receive({ - device.id, - clusters.ColorControl.attributes.ColorMode:build_test_report_data( - device, endpoint, color_mode) - }) - local read_req - if color_mode == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then - read_req = clusters.ColorControl.attributes.CurrentHue:read() - read_req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) - else -- color_mode = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - read_req = clusters.ColorControl.attributes.CurrentX:read() - read_req:merge(clusters.ColorControl.attributes.CurrentY:read()) - end - test.socket.matter:__expect_send({device.id, read_req}) -end - -local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.IlluminanceMeasurement.attributes.MeasuredValue, - clusters.OccupancySensing.attributes.Occupancy -} - -local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) -for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end -end - local function test_init() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - -- the following subscribe is due to the init event sent by the test framework. - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) - set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) end test.set_test_init_function(test_init) -local function test_init_x_y_color_mode() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) -end +local switch_fields = require "switch_utils.fields" -test.register_message_test( - "On command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 1) - } - } - }, - { - min_api_version = 17 - } +test.register_coroutine_test( + "doConfigure lifecycle event should not re-configure the device profile", + function () + mock_device:set_field(switch_fields.profiling_data.BATTERY_SUPPORT, false, {persist = true}) + mock_device:set_field(switch_fields.profiling_data.POWER_TOPOLOGY, false, {persist = true}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) -test.register_message_test( - "Off command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "off", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 1) - } - } - }, - { - min_api_version = 17 +test.register_coroutine_test( + "init should cause device to subscribe to all appropriate clusters", function() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.IlluminanceMeasurement.attributes.MeasuredValue, + clusters.OccupancySensing.attributes.Occupancy } -) - -test.register_message_test( - "Set level command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 1, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end -test.register_message_test( - "Current level reports should generate appropriate events", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, 1, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - }, - { - min_api_version = 17 - } + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + end, + { + min_api_version = 17 + } ) -local hue = math.floor((50 * 0xFE) / 100.0 + 0.5) -local sat = math.floor((50 * 0xFE) / 100.0 + 0.5) - test.register_message_test( - "Set color command should send huesat commands when supported", + "Illuminance reports should generate correct messages", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorCapabilities:build_test_report_data(mock_device, 1, 0x01) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation:build_test_command_response(mock_device, 1) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, hue) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, sat) + clusters.IlluminanceMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 2, 21370) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } - } - }, + message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + } }, { min_api_version = 17 @@ -391,268 +136,116 @@ test.register_message_test( ) test.register_message_test( - "Set Hue command should send MoveToHue", + "Occupancy reports should generate correct messages", { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "colorControl", component = "main", command = "setHue", args = { 50 } } - } - }, { channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set Saturation command should send MoveToSaturation", - { - { - channel = "capability", direction = "receive", message = { mock_device.id, - { capability = "colorControl", component = "main", command = "setSaturation", args = { 50 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 1) } }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color temperature should send the appropriate commands", - { { channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, 1) - } + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) }, { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 556) + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 0) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } }, { min_api_version = 17 } ) -test.register_coroutine_test( - "X and Y color values should report hue and saturation once both have been received", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(50) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(72) - ) - ) - end, - { - test_init = test_init_x_y_color_mode, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "X and Y color values have 0 value", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 0) - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 0) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(33) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(100) - ) - ) - end, - { - test_init = test_init_x_y_color_mode, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Y and X color values should report hue and saturation once both have been received", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(50) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(72) - ) - ) - end, - { - test_init = test_init_x_y_color_mode, - min_api_version = 17 - } -) - -test.register_message_test( - "Do not report when receiving a color temperature of 0 mireds", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 0) - } - } +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, - { - min_api_version = 17 + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode } -) +} -test.register_message_test( - "Illuminance reports should generate correct messages", - { +local mock_device_light_level_motion = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-motion.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.IlluminanceMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 2, 21370) + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light } }, { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + endpoint_id = 2, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } } - }, - { - min_api_version = 17 } -) +}) -test.register_message_test( - "Occupancy reports should generate correct messages", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 1) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 0) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) +local function test_init_light_level_motion() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_light_level_motion) +end + +test.register_coroutine_test( + "Test init and doConfigure for Dimmable Light device type with Occupancy Sensor", + function() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.OccupancySensing.attributes.Occupancy } - }, + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_light_level_motion) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) + end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) + test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_light_level_motion.id, + clusters.LevelControl.attributes.Options:write(mock_device_light_level_motion, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) + mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, { - min_api_version = 17 + test_init = test_init_light_level_motion, + min_api_version = 17 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 86ce8185bf..061f2e2f47 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -6,9 +6,10 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button -local utils = require "st.utils" -local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local st_utils = require "st.utils" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button.yml"), @@ -40,162 +41,74 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) -local mock_device_battery = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("button-battery.yml"), - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - matter_version = {hardware = 1, software = 1}, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER", - }, - { - cluster_id = clusters.PowerSource.ID, - cluster_type = "SERVER", - feature_map = clusters.PowerSource.types.Feature.BATTERY - }, - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - } - } -}) -local function expect_configure_buttons(device) - test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) -end +local expected_component_to_endpoint_map = { + ["main"] = 1 +} +local expected_initial_press_only_state = true -local function update_profile() - test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( - mock_device_battery, 1, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} - )}) - expect_configure_buttons(mock_device_battery) - mock_device_battery:expect_metadata_update({ profile = "button-battery" }) +local function expect_configure_button(device) + test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) end -local function test_init() - local CLUSTER_SUBSCRIBE_LIST = { - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - } - - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end - end - +local function test_init_for_lifecycle_tests() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - expect_configure_buttons(mock_device) - mock_device:expect_metadata_update({ profile = "button" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -local function test_init_battery() - local CLUSTER_SUBSCRIBE_LIST_BATTERY = { - clusters.PowerSource.server.attributes.AttributeList, - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - } - - local subscribe_request = CLUSTER_SUBSCRIBE_LIST_BATTERY[1]:subscribe(mock_device_battery) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_BATTERY) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end - end - +local function test_init_for_message_tests() test.disable_startup_messages() - test.mock_device.add_test_device(mock_device_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) - mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.mock_device.add_test_device(mock_device) + mock_device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, expected_component_to_endpoint_map, { persist = true }) + switch_utils.set_field_for_endpoint(mock_device, fields.INITIAL_PRESS_ONLY, 1, expected_initial_press_only_state, { persist = true }) end - -test.set_test_init_function(test_init) +test.set_test_init_function(test_init_for_message_tests) test.register_coroutine_test( - "Simulate the profile change update taking affect and the device info changing", - function() - test.socket.matter:__set_channel_ordering("relaxed") - update_profile() - test.wait_for_events() - local device_info_copy = utils.deep_copy(mock_device_battery.raw_st_data) - device_info_copy.profile.id = "buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "infoChanged", device_info_json }) - -- due to the AttributeList being processed in update_profile, setting profiling_data.BATTERY_SUPPORT, - -- subsequent subscriptions will not include AttributeList. - local UPDATED_CLUSTER_SUBSCRIBE_LIST = { - clusters.PowerSource.server.attributes.BatPercentRemaining, + "Handle initial init lifecycle event", + function () + local CLUSTER_SUBSCRIBE_LIST = { clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.ShortRelease, clusters.Switch.server.events.MultiPressComplete, } - local updated_subscribe_request = UPDATED_CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) - for i, clus in ipairs(UPDATED_CLUSTER_SUBSCRIBE_LIST) do - if i > 1 then updated_subscribe_request:merge(clus:subscribe(mock_device_battery)) end + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device_battery.id, updated_subscribe_request}) - expect_configure_buttons(mock_device_battery) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.wait_for_events() + assert(mock_device:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as not needing battery support") + assert(mock_device:get_field(fields.profiling_data.POWER_TOPOLOGY) == false, "Device should be marked as not needing to configure power topology") end, { - test_init = test_init_battery, + test_init = test_init_for_lifecycle_tests, min_api_version = 17 } ) test.register_coroutine_test( - "Handle received BatPercentRemaining from device.", - function() - update_profile() - test.socket.matter:__queue_receive( - { - mock_device_battery.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device_battery, 1, 150 - ) - } - ) - test.socket.capability:__expect_send( - mock_device_battery:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ) - ) + "Handle initial doConfigure lifecycle event, where profiling should be skipped", + function () + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, { persist = true }) + mock_device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + expect_configure_button(mock_device) + mock_device:expect_metadata_update({ profile = "button" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + assert(switch_utils.deep_equals(st_utils.deep_copy(mock_device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP)), expected_component_to_endpoint_map), "Component to endpoint map should be set in doConfigure") + assert(switch_utils.get_field_for_endpoint(mock_device, fields.INITIAL_PRESS_ONLY, 1) == expected_initial_press_only_state, "InitialPressOnly should be true") + assert(switch_utils.get_field_for_endpoint(mock_device, fields.SUPPORTS_MULTI_PRESS, 1) == nil, "MultiPress should be nil") + assert(switch_utils.get_field_for_endpoint(mock_device, fields.EMULATE_HELD, 1) == nil, "EmulateHeld should be nil") end, { - test_init = test_init_battery, + test_init = test_init_for_lifecycle_tests, min_api_version = 17 } ) @@ -498,67 +411,190 @@ test.register_message_test( } ) -local function reset_battery_profiling_info() - local fields = require "switch_utils.fields" - mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +local mock_device_battery = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("button-battery.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER", + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +local function test_init_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) +end + +local function test_init_profile_change_with_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) + -- by this point, init will have run and this will have been set to false + mock_device_battery:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) end test.register_coroutine_test( - "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + "Handle initial init lifecycle event for device with battery", + function () + local CLUSTER_SUBSCRIBE_LIST_BATTERY = { + clusters.PowerSource.server.attributes.AttributeList, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_BATTERY[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_BATTERY) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + assert(mock_device_battery:get_field(fields.profiling_data.BATTERY_SUPPORT) == nil, "Device should not have specified battery support yet") + assert(mock_device_battery:get_field(fields.profiling_data.POWER_TOPOLOGY) == nil, "Device should be marked as not needing to configure power topology") + end, + { + test_init = test_init_battery, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Following init events, after battery has been configured, device should not create subscriptions to AttributeList", function() - reset_battery_profiling_info() + mock_device_battery:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist = true}) test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) - } - ) + local CLUSTER_SUBSCRIBE_LIST_BATTERY = { + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_BATTERY[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_BATTERY) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) end, { - min_api_version = 17 + test_init = test_init_profile_change_with_battery, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle initial doConfigure lifecycle event for device with battery, where profiling should be skipped", + function () + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = test_init_profile_change_with_battery, + min_api_version = 17 } ) test.register_coroutine_test( - "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", + "PowerSource AttributeList with BatPercentRemaining update should trigger profile update to button-battery", + function() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 1, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_button(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "button-battery" }) + end, + { + test_init = test_init_profile_change_with_battery, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "PowerSource AttributeList with BatChargeLevel update should trigger profile update to button-batteryLevel", + function() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 1, {uint32(clusters.PowerSource.attributes.BatChargeLevel.ID)} + )}) + expect_configure_button(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "button-batteryLevel" }) + end, + { + test_init = test_init_profile_change_with_battery, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "PowerSource AttributeList with no attributes update should set battery support to false and trigger profile update to button", function() - reset_battery_profiling_info() - test.wait_for_events() test.socket.matter:__queue_receive( { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( - clusters.PowerSource.attributes.BatChargeLevel.ID - )}) + mock_device_battery.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_battery, 1, {uint32(2)}) } ) - expect_configure_buttons(mock_device) - mock_device:expect_metadata_update({ profile = "button-batteryLevel" }) + expect_configure_button(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "button" }) + test.wait_for_events() + assert(mock_device_battery:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as not supporting battery") end, { - min_api_version = 17 + test_init = test_init_profile_change_with_battery, + min_api_version = 17 } ) test.register_coroutine_test( - "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + "BatPercentRemaining attribute event should generate a battery capability report", function() - reset_battery_profiling_info() - test.wait_for_events() test.socket.matter:__queue_receive( { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( - clusters.PowerSource.attributes.BatPercentRemaining.ID - )}) + mock_device_battery.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device_battery, 1, 150 + ) } ) - expect_configure_buttons(mock_device) - mock_device:expect_metadata_update({ profile = "button-battery" }) + test.socket.capability:__expect_send( + mock_device_battery:generate_test_message( + "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) + ) + ) end, { - min_api_version = 17 + test_init = test_init_battery, + min_api_version = 17 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 626d5b0b8a..d443bdc573 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -119,6 +119,45 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_no_per_zone_sensitivity = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + } + } +}) + local subscribe_request local subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.AttributeList, @@ -169,6 +208,27 @@ end test.set_test_init_function(test_init) + +local subscribe_request_no_per_zone_sensitivity +local subscribed_attributes_no_per_zone_sensitivity = { + clusters.CameraAvStreamManagement.attributes.AttributeList, +} + +local function test_init_no_per_zone_sensitivity() + test.mock_device.add_test_device(mock_device_no_per_zone_sensitivity) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "init" }) + subscribe_request_no_per_zone_sensitivity = subscribed_attributes_no_per_zone_sensitivity[1]:subscribe(mock_device_no_per_zone_sensitivity) + subscribe_request_no_per_zone_sensitivity:merge(cluster_base.subscribe(mock_device_no_per_zone_sensitivity, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request_no_per_zone_sensitivity:merge(cluster_base.subscribe(mock_device_no_per_zone_sensitivity, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, camera_fields.ZoneManagementFeatureMapAttr.ID)) + for i, attr in ipairs(subscribed_attributes_no_per_zone_sensitivity) do + if i > 1 then subscribe_request_no_per_zone_sensitivity:merge(attr:subscribe(mock_device_no_per_zone_sensitivity)) end + end + test.socket.matter:__expect_send({mock_device_no_per_zone_sensitivity.id, subscribe_request_no_per_zone_sensitivity}) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "doConfigure" }) + mock_device_no_per_zone_sensitivity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, @@ -346,7 +406,83 @@ local function update_device_profile() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) +end + +local additional_subscribed_attributes_no_per_zone_sensitivity = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, + clusters.OnOff.attributes.OnOff, +} + +local function update_device_profile_no_per_zone_sensitivity() + local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "videoStreamSettings", + "zoneManagement", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + } + }, + profile = "camera" +} + + test.socket.matter:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_no_per_zone_sensitivity, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device_no_per_zone_sensitivity:expect_metadata_update(expected_metadata) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_no_per_zone_sensitivity:generate_info_changed({ profile = updated_device_profile })) + + test.socket.capability:__expect_send( + mock_device_no_per_zone_sensitivity:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation"} + )) + ) + + test.socket.capability:__expect_send( + mock_device_no_per_zone_sensitivity:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports"} + )) + ) + + for _, attr in ipairs(additional_subscribed_attributes_no_per_zone_sensitivity) do + subscribe_request_no_per_zone_sensitivity:merge(attr:subscribe(mock_device_no_per_zone_sensitivity)) + end + test.socket.matter:__expect_send({mock_device_no_per_zone_sensitivity.id, subscribe_request_no_per_zone_sensitivity}) end -- Matter Handler UTs @@ -1335,6 +1471,47 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Duplicate ZoneTriggered events should not duplicate triggeredZones state", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}})) + ) + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneStopped:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventStoppedReasonEnum.ACTION_STOPPED + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({})) + ) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Button events should generate appropriate events", function() @@ -1872,7 +2049,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive({ mock_device.id, { capability = "zoneManagement", component = "main", command = "newZone", args = { - i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "blue" + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "#FFFFFF" }} }) test.socket.matter:__expect_send({ @@ -1885,7 +2062,7 @@ test.register_coroutine_test( clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) }, - color = "blue" + color = "#FFFFFF" } ) ) @@ -2081,6 +2258,98 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Removing a zone with an existing trigger should send RemoveTrigger followed by RemoveZone", + function() + update_device_profile() + test.wait_for_events() + + -- Create a zone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + "motion zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}}}, "motion", "#FFFFFF" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct({ + name = "motion zone", + use = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + color = "#FFFFFF" + }) + ) + }) + + -- Create a trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3, + sensitivity = 5 + }) + }) + + -- Receive the Triggers attribute update from the device reflecting the new trigger + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, initial_duration = 10, augmentation_duration = 3, + max_duration = 15, blind_duration = 3, sensitivity = 5 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({{ + zoneId = 1, initialDuration = 10, augmentationDuration = 3, + maxDuration = 15, blindDuration = 3, sensitivity = 5 + }})) + ) + test.wait_for_events() + + -- Receive removeZone command: since a trigger exists for zone 1, RemoveTrigger is sent first, then RemoveZone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { 1 } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveTrigger(mock_device, CAMERA_EP, 1) + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, 1) + }) + test.wait_for_events() + + -- Receive the updated Zones attribute from the device with the zone removed + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Zones:build_test_report_data(mock_device, CAMERA_EP, {}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.zones({value = {}})) + ) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "setStream with label and viewport changes should emit capability event", function() @@ -2909,12 +3178,59 @@ test.register_coroutine_test( } mock_device:expect_metadata_update(updated_expected_metadata) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) end, { min_api_version = 17 } ) +test.register_coroutine_test( + "Zone Management trigger reports should omit sensitivity when per-zone sensitivity is unsupported, even if provided by client", + function() + update_device_profile_no_per_zone_sensitivity() + test.wait_for_events() + -- Create a trigger with + test.socket.capability:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device_no_per_zone_sensitivity.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device_no_per_zone_sensitivity, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3 + }) + }) + end, + { + test_init = test_init_no_per_zone_sensitivity, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Zone Management setSensitivity command should handle global sensitivity when per-zone sensitivity is unsupported", + function() + update_device_profile_no_per_zone_sensitivity() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + { capability = "zoneManagement", component = "main", command = "setSensitivity", args = { 5 } } + }) + test.socket.matter:__expect_send({ + mock_device_no_per_zone_sensitivity.id, + clusters.ZoneManagement.attributes.Sensitivity:write(mock_device_no_per_zone_sensitivity, CAMERA_EP, 5) + }) + end, + { + test_init = test_init_no_per_zone_sensitivity, + min_api_version = 17 + } +) + -- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua new file mode 100644 index 0000000000..3acf4129f9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua @@ -0,0 +1,475 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local endpoints = { + ROOT_EP = 0, + IRRIGATION_SYSTEM_EP = 1, + VALVE_1_EP = 2, + VALVE_2_EP = 3, + VALVE_3_EP = 4 +} + +-- Mock device representing an irrigation system with 3 valve endpoints +local mock_irrigation_system = test.mock_device.build_test_matter_device({ + label = "Matter Irrigation System", + profile = t_utils.get_profile_definition("irrigation-system.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = endpoints.ROOT_EP, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = endpoints.IRRIGATION_SYSTEM_EP, + clusters = { + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FlowMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.OperationalState.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0040, device_type_revision = 1} -- Irrigation System + } + }, + { + endpoint_id = endpoints.VALVE_1_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_2_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_3_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + } + } +}) + +local mock_children = {} +for _, endpoint in ipairs(mock_irrigation_system.endpoints) do + if endpoint.endpoint_id == 3 or endpoint.endpoint_id == 4 then + local child_data = { + profile = t_utils.get_profile_definition("water-valve-level.yml"), + device_network_id = string.format("%s:%d", mock_irrigation_system.id, endpoint.endpoint_id), + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local subscribe_request + +local expected_metadata = { + optional_component_capabilities = { { "main", { "level", "flowMeasurement", "operationalState", } } }, + profile = "irrigation-system" +} + +local function test_init() + test.mock_device.add_test_device(mock_irrigation_system) + local cluster_subscribe_list = { + clusters.ValveConfigurationAndControl.attributes.CurrentState, + clusters.ValveConfigurationAndControl.attributes.CurrentLevel, + } + subscribe_request = cluster_subscribe_list[1]:subscribe(mock_irrigation_system) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_irrigation_system)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "init" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "doConfigure" }) + mock_irrigation_system:expect_metadata_update(expected_metadata) + mock_irrigation_system:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + for i = 3,4 do + mock_irrigation_system:expect_device_create({ + type = "EDGE_CHILD", + label = string.format("Matter Irrigation System %d", i - 1), + profile = "irrigation-system", + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", i) + }) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end +test.set_test_init_function(test_init) + + +local additional_subscribed_attributes = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.OperationalState, +} + +local function update_device_profile() + local updated_device_profile = t_utils.get_profile_definition( + "irrigation-system.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities } + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_irrigation_system:generate_info_changed({ profile = updated_device_profile })) + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_irrigation_system)) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end + +test.register_coroutine_test( + "Parent device: Open command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Close command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Set level command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "level", component = "main", command = "setLevel", args = { 75 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP, nil, 75) + }) + end +) + +test.register_coroutine_test( + "Parent device: Current state closed should generate closed event", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Parent device: Current level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 60 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.level.level(60)) + ) + end +) + +test.register_coroutine_test( + "Flow reports should generate correct messages", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20 * 10) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flow({ value = 20.0, unit = "m^3/h" }) + ) + ) + end +) + +test.register_coroutine_test( + "Min and max flow attributes set capability constraint", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20) + }) + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 5000) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flowRange({ + value = { minimum = 2.0, maximum = 500.0 }, + unit = "m^3/h" + }) + ) + ) + end +) + +test.register_coroutine_test( + "Child device valve 2: Open command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Set level command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "level", component = "main", command = "setLevel", args = { 40 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP, nil, 40) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Current state closed should generate closed event", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_2_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_2_EP]:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Child device valve 3: Close command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_3_EP].id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_3_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 3: Current level reports should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_3_EP, + 100 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_3_EP]:generate_test_message("main", capabilities.level.level(100)) + ) + end +) + +test.register_coroutine_test( + "OperationalState attribute running should generate running event", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, + clusters.OperationalState.types.OperationalStateEnum.RUNNING + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.running()) + ) + end +) + +test.register_coroutine_test( + "OperationalState OperationalError UNABLE_TO_COMPLETE_OPERATION should generate unableToCompleteOperation event", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, { error_state_id = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION } + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.unableToCompleteOperation()) + ) + end +) + +test.register_coroutine_test( + "OperationalState resume command should send Resume and re-read state/error to irrigation system EP", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "operationalState", component = "main", command = "resume", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.server.commands.Resume(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + end +) + +test.register_coroutine_test( + "OperationalState pause command should send Pause and re-read state/error to irrigation system EP", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "operationalState", component = "main", command = "pause", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.server.commands.Pause(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + end +) + +test.register_coroutine_test( + "Child device doConfigure: update child profile based on its endpoint configuration", + function() + test.socket.device_lifecycle:__queue_receive({ mock_children[endpoints.VALVE_2_EP].id, "doConfigure" }) + mock_children[endpoints.VALVE_2_EP]:expect_metadata_update({ + profile = "irrigation-system", + optional_component_capabilities = {{"main", {"level"}}} + }) + mock_children[endpoints.VALVE_2_EP]:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + end +) + +test.run_registered_tests() + diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua index 6a8ab5657a..32023a36cb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua @@ -5,11 +5,6 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.generated.zap_clusters" local t_utils = require "integration_test.utils" local test = require "integration_test" -local version = require "version" - -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device_ep1 = 1 local mock_device_ep2 = 2 @@ -127,9 +122,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- since all fan capabilities are optional, nothing is initially subscribed to - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -176,98 +168,6 @@ test.register_coroutine_test( { test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end } ) - -test.register_coroutine_test( - "Switch capability should send the appropriate commands", function() - test.socket.capability:__queue_receive( - { - mock_child.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - ) - if version.api >= 11 then - test.socket.devices:__expect_send( - { - "register_native_capability_cmd_handler", - { device_uuid = mock_child.id, capability_id = "switch", capability_cmd_id = "on" } - } - ) - end - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, mock_device_ep1) - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, mock_device_ep1, true) - } - ) - test.socket.devices:__expect_send( - { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - ) - test.socket.capability:__expect_send( - mock_child:generate_test_message( - "main", capabilities.switch.switch.on() - ) - ) - end, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_child.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep1) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep1, 556) - } - }, - { - channel = "capability", - direction = "send", - message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, - }, - { - min_api_version = 17 - } -) - local FanMode = clusters.FanControl.attributes.FanMode test.register_message_test( "Fan mode reports should generate correct messages", diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index 2225ed129b..d7d5efde97 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -179,19 +179,10 @@ local mock_device_battery = test.mock_device.build_test_matter_device( local function expect_configure_buttons(device) test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 50)}) - test.socket.capability:__expect_send(device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 60)}) - test.socket.capability:__expect_send(device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end local function update_profile() @@ -221,10 +212,6 @@ local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated - -- added sets a bunch of fields on the device, and calls init - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) @@ -254,9 +241,6 @@ local function test_init_battery() test.disable_startup_messages() test.mock_device.add_test_device(mock_device_battery) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) - test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua index 68fc7e8339..44ee11861e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -125,22 +125,11 @@ local CLUSTER_SUBSCRIBE_LIST ={ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.pushed({state_change = false}))) end -- All messages queued and expectations set are done before the driver is actually run @@ -155,8 +144,6 @@ local function test_init() for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 0c4b314791..70f9666542 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -7,12 +7,8 @@ local t_utils = require "integration_test.utils" local clusters = require "st.matter.generated.zap_clusters" -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 0x01 local button_attr = capabilities.button.button - local mock_device_ep1 = 1 local mock_device_ep2 = 2 local mock_device_ep3 = 3 @@ -198,13 +194,8 @@ local CLUSTER_SUBSCRIBE_LIST_WITH_CHILD ={ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end local function test_init() @@ -216,8 +207,6 @@ local function test_init() for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) @@ -367,35 +356,6 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "Switch child device: Set color temperature should send the appropriate commands", - function() - test.mock_device.add_test_device(mock_child) - test.wait_for_events() - test.socket.capability:__queue_receive({ - mock_child.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - }) - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) - }) - test.wait_for_events() - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) - }) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Test MCD configuration not including switch for unsupported switch device type, create child device instead", function() @@ -411,8 +371,6 @@ test.register_coroutine_test( for _, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end - test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) - test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) @@ -426,7 +384,6 @@ test.register_coroutine_test( parent_assigned_child_key = string.format("%d", 7) }) test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) unsup_mock_device:expect_metadata_update({ profile = "2-button" }) unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -450,10 +407,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) end, { test_init = test_init_mcd_unsupported_switch_device_type, @@ -475,6 +429,7 @@ test.register_coroutine_test( mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) expect_configure_buttons() + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { min_api_version = 17 diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua new file mode 100644 index 0000000000..d27ee2c383 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua @@ -0,0 +1,303 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" + +test.disable_startup_messages() + + +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } +} + + +local mock_device_onoff_switch_as_server = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for onoff parent cluster as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "doConfigure" }) + mock_device_onoff_switch_as_server:expect_metadata_update({ profile = "switch-binary" }) + mock_device_onoff_switch_as_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_server) end, + min_api_version = 17 + } +) + + +local mock_device_onoff_switch_as_client = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch + } + } + } +}) + +test.register_coroutine_test( + "Test init for onoff parent cluster as client", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "doConfigure" }) + mock_device_onoff_switch_as_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_client) end, + min_api_version = 17 + } +) + + +local mock_device_dimmer_switch_as_server = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for dimmer parent cluster as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_dimmer_switch_as_server.id, + clusters.LevelControl.attributes.Options:write(mock_device_dimmer_switch_as_server, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_dimmer_switch_as_server:expect_metadata_update({ profile = "switch-level" }) + mock_device_dimmer_switch_as_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_dimmer_switch_as_server) end, + min_api_version = 17 + } +) + + +local mock_device_plug_with_switch_profile_vendor_override = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { vendor_id = 0x142B, product_id = 0x1003}, -- this device has a vendor override to join as a switch instead of a plug + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- OnOff PlugIn Unit + } + } + } +}) + +test.register_coroutine_test( + "Test init for device with requiring the switch category as a vendor override", + function() + local mock_device = mock_device_plug_with_switch_profile_vendor_override + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "switch-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_plug_with_switch_profile_vendor_override) end, + min_api_version = 17 + } +) + + +local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 7, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "CLIENT", feature_map = 31}, + + }, + device_types = { + {device_type_id = 0x0105, device_type_revision = 1} -- Color Dimmer Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for color dimmer device type as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) + mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) + mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_color_dimmer) end, + min_api_version = 17 + } +) + + +local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 7, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + + }, + device_types = { + {device_type_id = 0x010F, device_type_revision = 1} -- Mounted On/Off Control + } + } + } +}) + +test.register_coroutine_test( + "Test init for mounted onoff control", + function() + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_mounted_on_off_control) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) + test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_on_off_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_on_off_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) + mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_mounted_on_off_control) end, + min_api_version = 17 + } +) + + +local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 7, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + + }, + device_types = { + {device_type_id = 0x0110, device_type_revision = 1} -- Mounted Dimmable Load Control + } + } + } +}) + +test.register_coroutine_test( + "Test init for mounted dimmable load control", + function() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.LevelControl.attributes.MaxLevel, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_dimmable_load_control) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_mounted_dimmable_load_control)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) + test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_dimmable_load_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_dimmable_load_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end, + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua new file mode 100644 index 0000000000..608112d61c --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua @@ -0,0 +1,878 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +test.disable_startup_messages() + +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } +} + +local parent_ep_id = 10 +local dimmable_ep_id = 30 +local extended_color_ep_id = 50 + +-- this parent device would fingerprint as light-color-level, since the most feature-rich endpoint is the extended color one, +-- but it should re-configure to light-binary in doConfigure +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light + } + }, + { + endpoint_id = extended_color_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = dimmable_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } + }, + } +}) + +local child_profiles = { + [dimmable_ep_id] = t_utils.get_profile_definition("light-level.yml"), + [extended_color_ep_id] = t_utils.get_profile_definition("light-color-level.yml"), +} + +local mock_children = {} +for i, endpoint in ipairs(mock_device.endpoints) do + if endpoint.endpoint_id ~= parent_ep_id and endpoint.endpoint_id ~= 0 then + local child_data = { + profile = child_profiles[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function handle_init_event(mock_device) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, + } + local expected_subscriptions = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + expected_subscriptions:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, expected_subscriptions}) +end + +local function handle_do_configure_event(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, extended_color_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, extended_color_ep_id, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, dimmable_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + + mock_device:expect_metadata_update({ profile = "light-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", dimmable_ep_id) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 3", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", extended_color_ep_id) + }) +end + +local function test_init_for_lifecycle_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end +end + +-- due to device copy logic in the integration tests, we need to handle init and doConfigure before generating an infoChanged event +local function test_init_for_generate_info_changed_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + handle_init_event(mock_device) + handle_do_configure_event(mock_device) +end + +local function test_init_for_post_configure_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + local FIND_CHILD_KEY = "__find_child_fn" + mock_device:set_field(FIND_CHILD_KEY, switch_utils.find_child, { persist = false }) + mock_device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, { persist = false }) +end + +test.set_test_init_function(test_init_for_post_configure_tests) + +test.register_coroutine_test( + "Handle initial init lifecycle event, before children are created", + function() + handle_init_event(mock_device) + test.wait_for_events() + assert(mock_device:get_field(fields.profiling_data.POWER_TOPOLOGY) == false, "Device should be marked as not needing to configure power topology") + assert(mock_device:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as having no battery") + end, + { + test_init = test_init_for_lifecycle_tests, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle doConfigure lifecycle event", + function() + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event(mock_device) + test.wait_for_events() + local FIND_CHILD_KEY = "__find_child_fn" + assert(type(mock_device:get_field(FIND_CHILD_KEY)) == "function", "Child find function should be stored in doConfigure") + end, + { + test_init = test_init_for_lifecycle_tests, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 + mock_children[dimmable_ep_id]:expect_metadata_update({ profile = "light-level" }) + mock_children[extended_color_ep_id]:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) + end, + { + test_init = test_init_for_generate_info_changed_tests, + min_api_version = 17 + } +) + + +test.register_message_test( + "Dimmable Child: Current level cluster reports generate switch level events appropriately", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, dimmable_ep_id, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Children: Level Control Min and max attributes set switch level constraints appropriately", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, dimmable_ep_id, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, dimmable_ep_id, 254) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, extended_color_ep_id, 127) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, extended_color_ep_id, 203) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Extended Color Child: SetColor command should be handled correctly", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor(mock_device, extended_color_ep_id, 15182, 21547, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, extended_color_ep_id) + } + }, + } +) + +test.register_message_test( + "Extended Color Child: X and Y color values should report hue and saturation once both have been received", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, extended_color_ep_id, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, extended_color_ep_id, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Extended Color Child: colorTemperatureRange, setColorTemperature, colorTemperature, stepColorTemperatureByPercent handled appropriately", + { + -- colorTemperatureRange testing + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, extended_color_ep_id, 153) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, extended_color_ep_id, 555) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + }, + + -- setColorTemperature testing + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[extended_color_ep_id].id, capability_id = "colorTemperature", capability_cmd_id = "setColorTemperature" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, extended_color_ep_id, 555, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + } + }, -- 555 is expected since it is re-bounded by the given range + + -- colorTemperature testing + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, extended_color_ep_id, 555) + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_attr_id = "colorTemperature" } + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + + -- stepColorTemperatureByPercent testing + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[extended_color_ep_id].id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device, extended_color_ep_id, clusters.ColorControl.types.StepModeEnum.DOWN, 80, fields.DEFAULT_STEP_TRANSITION_TIME, 153, 555, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Parent: switch capability <-> On Off cluster should handle events appropriately", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, parent_ep_id) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep_id, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Children: switch capability <-> On Off Cluster should handle events appropriately", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[dimmable_ep_id].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[dimmable_ep_id].id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, dimmable_ep_id) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, dimmable_ep_id, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[extended_color_ep_id].id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, extended_color_ep_id) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, extended_color_ep_id, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + }, + { + min_api_version = 17 + } +) + +local dimmable_child_plug_ep_id = 30 + +local mock_plug = test.mock_device.build_test_matter_device({ + label = "Matter Plug", + profile = t_utils.get_profile_definition("plug-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + { + endpoint_id = dimmable_child_plug_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + } +}) + +local function handle_init_event_for_plug(mock_device) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + } + local expected_subscriptions = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + expected_subscriptions:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, expected_subscriptions}) +end + +local function handle_do_configure_event_for_plug(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, dimmable_child_plug_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ profile = "plug-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Plug 2", + profile = "plug-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", dimmable_child_plug_ep_id) + }) +end + +test.register_coroutine_test( + "Plug: Handle initial init lifecycle event, before children are created", + function() + handle_init_event_for_plug(mock_plug) + test.wait_for_events() + assert(mock_plug:get_field(fields.profiling_data.POWER_TOPOLOGY) == false, "Device should be marked as not needing to configure power topology") + assert(mock_plug:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as having no battery") + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug) + end, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Plug: Handle doConfigure lifecycle event", + function() + mock_plug:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_plug:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event_for_plug(mock_plug) + test.wait_for_events() + local FIND_CHILD_KEY = "__find_child_fn" + assert(type(mock_plug:get_field(FIND_CHILD_KEY)) == "function", "Child find function should be stored in doConfigure") + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug) + end, + min_api_version = 17 + } +) + + +local overriden_plug_child_ep_id = 30 + +-- This device overrides both its parent and child profiles to become the Switch category +local mock_plug_profile_override = test.mock_device.build_test_matter_device({ + label = "Matter Plug", + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { vendor_id = 0x1321, product_id = 0x000C }, -- this Sonoff device has an overloaded profile only for its children + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + { + endpoint_id = overriden_plug_child_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + } +}) + + +local function handle_do_configure_event_for_plug_with_profile_override(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "switch-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Plug 2", + profile = "switch-binary", -- overriden profile for Sonoff device + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", overriden_plug_child_ep_id) + }) +end + +test.register_coroutine_test( + "Plug with Overriden Profile: Handle doConfigure lifecycle event", + function() + mock_plug_profile_override:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_plug_profile_override:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event_for_plug_with_profile_override(mock_plug_profile_override) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug_profile_override) + end, + min_api_version = 17 + } +) + +local mock_switch = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 7, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.DIMMER, device_type_revision = 1} + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT, device_type_revision = 1} + } + }, + { + endpoint_id = 20, -- this endpoint should not generate a child device since it only has a client OnOff cluster + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT, device_type_revision = 1} + } + }, + { + endpoint_id = 30, -- this endpoint should profile correctly, though it is not a switch + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.ON_OFF, device_type_revision = 2} + } + }, + { + endpoint_id = 40, -- this endpoint should generate a switch-binary child device since it has a SERVER OnOff cluster,though the device type is unknown. + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0304, device_type_revision = 2} -- Pump Controller + } + } + } +}) + +test.register_coroutine_test( + "Switch Profile: Handle doConfigure lifecycle event", + function() + mock_switch:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_switch:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + test.socket.device_lifecycle:__queue_receive({ mock_switch.id, "doConfigure" }) + test.socket.matter:__expect_send({ mock_switch.id, clusters.LevelControl.attributes.Options:write(mock_switch, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + test.socket.matter:__expect_send({ mock_switch.id, clusters.LevelControl.attributes.Options:write(mock_switch, 40, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + mock_switch:expect_metadata_update({ profile = "switch-level" }) + mock_switch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "switch-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 10) + }) + + -- client cluster only endpoint (20) should not generate a child device + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 3", + profile = "light-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 30) + }) + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "switch-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 40) + }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_switch) end, + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua index 3b3cefd8e3..eabc9246f7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua @@ -4,8 +4,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -40,50 +38,11 @@ local mock_device = test.mock_device.build_test_matter_device({ local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) - - local cluster_subscribe_list = { - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete, - - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.PowerSource.attributes.BatPercentRemaining - } - - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json}) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - end test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", function() local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,1, 2000) - mock_device.st_store.preferences = {tempOffset = "0"} + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { tempOffset = "2" } })) test.socket.matter:__queue_receive({mock_device.id, report}) test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ @@ -108,7 +67,7 @@ end, test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", function() local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,2, 2000) - mock_device.st_store.preferences = {humidityOffset = "0"} + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { humidityOffset = "0" } })) test.socket.matter:__queue_receive({mock_device.id, report}) test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index a1b60be357..e68f1d0101 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -4,7 +4,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local st_utils = require "st.utils" +local fields = require "switch_utils.fields" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 @@ -27,87 +27,18 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x0016, device_type_revision = 1} -- RootNode } }, - { - endpoint_id = 1, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0, --u32 bitmap - }, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light - } - } - } -}) - -local mock_device_no_hue_sat = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-color-level.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1} -- On/Off Light - } - } - } -}) - -local mock_device_color_temp = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light - {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light - } - } - } -}) - -local mock_device_extended_color = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("light-color-level.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { { endpoint_id = 1, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 1}, -- Dimmable Light - {device_type_id = 0x010C, device_type_revision = 1}, -- Color Temperature Light - {device_type_id = 0x010D, device_type_revision = 1}, -- Extended Color Light + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.ON_OFF, device_type_revision = 1}, + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE, device_type_revision = 1}, + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE, device_type_revision = 1}, + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR, device_type_revision = 1} } } } @@ -129,151 +60,56 @@ local cluster_subscribe_list = { clusters.ColorControl.attributes.ColorMode, } -local function set_color_mode(device, endpoint, color_mode) - test.socket.matter:__queue_receive({ - device.id, - clusters.ColorControl.attributes.ColorMode:build_test_report_data( - device, endpoint, color_mode) - }) - local read_req - if color_mode == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then - read_req = clusters.ColorControl.attributes.CurrentHue:read() - read_req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) - else -- color_mode = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - read_req = clusters.ColorControl.attributes.CurrentX:read() - read_req:merge(clusters.ColorControl.attributes.CurrentY:read()) - end - test.socket.matter:__expect_send({device.id, read_req}) -end - local function test_init() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - -- note that since disable_startup_messages is not explicitly called here, - -- the following subscribe is due to the init event sent by the test framework. - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) - set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) -end -test.set_test_init_function(test_init) -local function test_init_x_y_color_mode() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) -end - -local function test_init_no_hue_sat() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_no_hue_sat) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_no_hue_sat)) - end - end - test.socket.device_lifecycle:__queue_receive({ mock_device_no_hue_sat.id, "added" }) - test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) - - test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_no_hue_sat) - set_color_mode(mock_device_no_hue_sat, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) -end - - -local cluster_subscribe_list_color_temp = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds -} - -local function test_init_color_temp() - test.mock_device.add_test_device(mock_device_color_temp) - local subscribe_request = cluster_subscribe_list_color_temp[1]:subscribe(mock_device_color_temp) - for i, cluster in ipairs(cluster_subscribe_list_color_temp) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) - test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) - test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_color_temp.id, - clusters.LevelControl.attributes.Options:write(mock_device_color_temp, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - test.socket.matter:__expect_send({ - mock_device_color_temp.id, - clusters.ColorControl.attributes.Options:write(mock_device_color_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) end -local function test_init_extended_color() - test.mock_device.add_test_device(mock_device_extended_color) - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_extended_color) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_extended_color)) - end - end - test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) - test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_extended_color.id, - clusters.LevelControl.attributes.Options:write(mock_device_extended_color, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - test.socket.matter:__expect_send({ - mock_device_extended_color.id, - clusters.ColorControl.attributes.Options:write(mock_device_extended_color, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) -end +test.set_test_init_function(test_init) -test.register_message_test( - "Test that Color Temperature Light device does not switch profiles", - {}, +test.register_coroutine_test( + "doConfigure properly sets Extended Color Light and does not attempt to switch profiles", + function() + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, { persist = true }) + mock_device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.LevelControl.attributes.Options:write(mock_device, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ColorControl.attributes.Options:write(mock_device, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, { - test_init = test_init_color_temp, min_api_version = 17 } ) -test.register_message_test( - "Test that Extended Color Light device does not switch profiles", - {}, +test.register_coroutine_test( + "init creates accurate subscription for Extended Color Light", + function() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + end, { - test_init = test_init_extended_color, min_api_version = 17 } ) @@ -344,23 +180,43 @@ test.register_message_test( } ) + +local hue = math.floor((50 * 0xFE) / 100.0 + 0.5) +local sat = math.floor((50 * 0xFE) / 100.0 + 0.5) test.register_message_test( - "Set level command should send the appropriate commands", + "Set Hue command should send MoveToHue", { { channel = "capability", direction = "receive", message = { mock_device.id, - { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } + { capability = "colorControl", component = "main", command = "setHue", args = { 50 } } } }, { - channel = "devices", + channel = "matter", direction = "send", message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } + mock_device.id, + clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + } + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Set Saturation command should send MoveToSaturation", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "colorControl", component = "main", command = "setSaturation", args = { 50 } } } }, { @@ -368,36 +224,56 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) + clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Set color temperature should send the appropriate commands", + { { - channel = "matter", + channel = "capability", direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_cmd_id = "setColorTemperature" } } }, { channel = "matter", - direction = "receive", + direction = "send", message = { mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 1, 50) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, 1) + } }, { channel = "devices", direction = "send", message = { "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_attr_id = "colorTemperature" } } }, { @@ -405,23 +281,39 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, true) + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 556) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Do not report when receiving a color temperature of 0 mireds", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 0) + } }, { channel = "devices", direction = "send", message = { "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_attr_id = "colorTemperature" } } - }, - + } }, { min_api_version = 17 @@ -429,276 +321,234 @@ test.register_message_test( ) test.register_message_test( - "Current level reports should generate appropriate events", + "Min and max color temperature attributes set capability constraint", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, 1, 50) + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 153) } }, { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(st_utils.round((50 / 254.0 * 100) + 0.5))) - }, - { - channel = "devices", - direction = "send", + channel = "matter", + direction = "receive", message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 555) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + } }, { min_api_version = 17 } ) -test.register_coroutine_test( - "Set color command should send the appropriate commands", function() - test.socket.capability:__queue_receive( - { - mock_device_no_hue_sat.id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } }}, +test.register_message_test( + "Min and max color temperature attributes set capability constraint using improved temperature conversion rounding", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) } - ) - test.socket.matter:__expect_send( - { - mock_device_no_hue_sat.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_no_hue_sat.id, - clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device_no_hue_sat, 1) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_no_hue_sat.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device_no_hue_sat, 1, 15091) - } - ) - test.socket.matter:__queue_receive( - { - mock_device_no_hue_sat.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device_no_hue_sat, 1, 21547) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) } - ) - test.socket.capability:__expect_send( - mock_device_no_hue_sat:generate_test_message( - "main", capabilities.colorControl.hue(50) - ) - ) - test.socket.capability:__expect_send( - mock_device_no_hue_sat:generate_test_message( - "main", capabilities.colorControl.saturation(72) - ) - ) - end, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) + } + }, { - test_init = test_init_no_hue_sat, - min_api_version = 17 + min_api_version = 17 } ) -local hue = math.floor((50 * 0xFE) / 100.0 + 0.5) -local sat = math.floor((50 * 0xFE) / 100.0 + 0.5) - test.register_message_test( - "Set color command should send huesat commands when supported", + "Device reports mireds outside of supported range, set capability to min/max value in kelvin", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorCapabilities:build_test_report_data(mock_device, 1, 0x01) + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) } }, { - channel = "capability", + channel = "matter", direction = "receive", message = { mock_device.id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } } } + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) } }, { - channel = "matter", + channel = "capability", direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) }, { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation:build_test_command_response(mock_device, 1) + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 160) } }, { - channel = "matter", - direction = "receive", + channel = "devices", + direction = "send", message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, hue) + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_attr_id = "colorTemperature" } } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } - } + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6000)) }, { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, sat) + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 370) } }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - }, { channel = "devices", direction = "send", message = { "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_attr_id = "colorTemperature" } } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2800)) + } }, { min_api_version = 17 } ) -hue = 0xFE -sat = 0xFE test.register_message_test( - "Set color command should clamp invalid huesat values", + "Capability sets color temp outside of supported range, value sent to device is limited to min/max value in mireds", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorCapabilities:build_test_report_data(mock_device, 1, 0x01) + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) } }, { - channel = "capability", + channel = "matter", direction = "receive", message = { mock_device.id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 110, saturation = 110 } } } + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) } }, { - channel = "matter", + channel = "capability", direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } + message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) }, { - channel = "matter", + channel = "capability", direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation:build_test_command_response(mock_device, 1) + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {6100} } } }, { - channel = "matter", - direction = "receive", + channel = "devices", + direction = "send", message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, hue) + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_cmd_id = "setColorTemperature" } } }, { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.hue(100)) - }, - { - channel = "devices", + channel = "matter", direction = "send", message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { - channel = "matter", + channel = "capability", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, sat) + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } } }, { - channel = "capability", + channel = "devices", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "colorTemperature", capability_cmd_id = "setColorTemperature" } + } }, { - channel = "devices", + channel = "matter", direction = "send", message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } - }, + } }, { min_api_version = 17 } ) -hue = math.floor((50 * 0xFE) / 100.0 + 0.5) -sat = math.floor((50 * 0xFE) / 100.0 + 0.5) test.register_message_test( - "Set Hue command should send MoveToHue", + "Min color temperature outside of range, capability not sent", { { - channel = "capability", + channel = "matter", direction = "receive", message = { mock_device.id, - { capability = "colorControl", component = "main", command = "setHue", args = { 50 } } + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 50) } }, { channel = "matter", - direction = "send", + direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 555) } - }, + } }, { min_api_version = 17 @@ -706,24 +556,24 @@ test.register_message_test( ) test.register_message_test( - "Set Saturation command should send MoveToSaturation", + "Max color temperature outside of range, capability not sent", { { - channel = "capability", + channel = "matter", direction = "receive", message = { mock_device.id, - { capability = "colorControl", component = "main", command = "setSaturation", args = { 50 } } + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 153) } }, { channel = "matter", - direction = "send", + direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 1100) } - }, + } }, { min_api_version = 17 @@ -731,30 +581,44 @@ test.register_message_test( ) test.register_message_test( - "Set color temperature should send the appropriate commands", + "Min and max level attributes set capability constraint", { { - channel = "capability", + channel = "matter", direction = "receive", message = { mock_device.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 5) } }, { channel = "matter", - direction = "send", + direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 10) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 2, maximum = 4})) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Min level attribute outside of range for lighting feature device (min level = 1), capability not sent", + { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, 1) + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 0) } }, { @@ -762,14 +626,9 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 556) + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 10) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, + } }, { min_api_version = 17 @@ -777,84 +636,116 @@ test.register_message_test( ) test.register_coroutine_test( - "X and Y color values should report hue and saturation once both have been received", + "colorControl capability sent based on CurrentX and CurrentY due to ColorMode", function() test.socket.matter:__queue_receive( { mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) + clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) } ) - test.socket.matter:__queue_receive( + local read_x_y = clusters.ColorControl.attributes.CurrentX:read() + read_x_y:merge(clusters.ColorControl.attributes.CurrentY:read()) + test.socket.matter:__expect_send( { mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) + read_x_y } ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(50) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(72) - ) - ) - end, - { - test_init = test_init_x_y_color_mode, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "X and Y color values have 0 value", - function() test.socket.matter:__queue_receive( { mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 0) + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) } ) test.socket.matter:__queue_receive( { mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 0) + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.colorControl.hue(33) + "main", capabilities.colorControl.hue(50) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(100) + "main", capabilities.colorControl.saturation(72) ) ) end, { - test_init = test_init_x_y_color_mode, - min_api_version = 17 + min_api_version = 17 } ) test.register_coroutine_test( - "Y and X color values should report hue and saturation once both have been received", + "Refresh necessary attributes", function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) - } + test.socket.capability:__queue_receive( + {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} ) + local read_request = cluster_subscribe_list[1]:read(mock_device) + for i, attr in ipairs(cluster_subscribe_list) do + if i > 1 then read_request:merge(attr:read(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, read_request}) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + + +local function set_color_mode(device, endpoint, color_mode) + test.socket.matter:__queue_receive({ + device.id, + clusters.ColorControl.attributes.ColorMode:build_test_report_data( + device, endpoint, color_mode) + }) + local read_req + if color_mode == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then + read_req = clusters.ColorControl.attributes.CurrentHue:read() + read_req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) + else -- color_mode = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY + read_req = clusters.ColorControl.attributes.CurrentX:read() + read_req:merge(clusters.ColorControl.attributes.CurrentY:read()) + end + test.socket.matter:__expect_send({device.id, read_req}) +end + + + +local function test_init_x_y_color_mode() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) +end + +test.register_coroutine_test( + "X and Y color values should report hue and saturation once both have been received", + function() test.socket.matter:__queue_receive( { mock_device.id, clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) } ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.colorControl.hue(50) @@ -872,148 +763,175 @@ test.register_coroutine_test( } ) -test.register_message_test( - "Do not report when receiving a color temperature of 0 mireds", - { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "X and Y color values have 0 value", + function() + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 0) + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 0) } - } - }, + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 0) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.hue(33) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.saturation(100) + ) + ) + end, { - min_api_version = 17 + test_init = test_init_x_y_color_mode, + min_api_version = 17 } ) -test.register_message_test( - "Min and max color temperature attributes set capability constraint", - { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Y and X color values should report hue and saturation once both have been received", + function() + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 153) + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 555) + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) - } - }, + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.hue(50) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.saturation(72) + ) + ) + end, { - min_api_version = 17 + test_init = test_init_x_y_color_mode, + min_api_version = 17 } ) -test.register_message_test( - "Min and max color temperature attributes set capability constraint using improved temperature conversion rounding", - { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "colorControl capability sent based on CurrentHue and CurrentSaturation due to ColorMode", + function() + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) + clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + local read_hue_sat = clusters.ColorControl.attributes.CurrentHue:read() + read_hue_sat:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) + test.socket.matter:__expect_send( + { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) + read_hue_sat } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) - } - }, + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, 0xFE), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.hue(100) + ) + ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, 0xFE), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.colorControl.saturation(100) + ) + ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + ) + end, { - min_api_version = 17 + test_init = test_init_x_y_color_mode, + min_api_version = 17 } ) + + +local function test_init_with_hue_sat_color_mode_set() + test.mock_device.add_test_device(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) +end + test.register_message_test( - "Device reports mireds outside of supported range, set capability to min/max value in kelvin", + "Set color command should send huesat commands when supported", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) + clusters.ColorControl.attributes.ColorCapabilities:build_test_report_data(mock_device, 1, 0x01) } }, { channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) - }, - { - channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 160) + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } } } } }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6000)) - }, { channel = "matter", - direction = "receive", + direction = "send", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, 1, 370) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(2800)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Capability sets color temp outside of supported range, value sent to device is limited to min/max value in mireds", - { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 165) + clusters.ColorControl.server.commands.MoveToHueAndSaturation:build_test_command_response(mock_device, 1) } }, { @@ -1021,86 +939,77 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 365) + clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, hue) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2800, maximum = 6000})) + message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) }, { - channel = "capability", - direction = "receive", + channel = "devices", + direction = "send", message = { - mock_device.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {6100} } + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } } }, { channel = "matter", - direction = "send", + direction = "receive", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, sat) } }, { channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {2700} } - } + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) }, { - channel = "matter", + channel = "devices", direction = "send", message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } } - } + }, }, { - min_api_version = 17 + test_init = test_init_with_hue_sat_color_mode_set, + min_api_version = 17 } ) +hue = 0xFE +sat = 0xFE test.register_message_test( - "Min color temperature outside of range, capability not sent", + "Set color command should clamp invalid huesat values", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 50) + clusters.ColorControl.attributes.ColorCapabilities:build_test_report_data(mock_device, 1, 0x01) } }, { - channel = "matter", + channel = "capability", direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 555) + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 110, saturation = 110 } } } } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Max color temperature outside of range, capability not sent", - { + }, { channel = "matter", - direction = "receive", + direction = "send", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, 1, 153) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -1108,24 +1017,28 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, 1, 1100) + clusters.ColorControl.server.commands.MoveToHueAndSaturation:build_test_command_response(mock_device, 1) } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min and max level attributes set capability constraint", - { + }, { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 5) + clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, hue) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.hue(100)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } } }, { @@ -1133,163 +1046,183 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 10) + clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, sat) } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 2, maximum = 4})) - } + message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, }, { - min_api_version = 17 + test_init = test_init_with_hue_sat_color_mode_set, + min_api_version = 17 } ) -test.register_message_test( - "Min level attribute outside of range for lighting feature device (min level = 1), capability not sent", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, 1, 0) - } - }, + + + +local mock_color_device_no_hue_sat = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, 1, 10) + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR, device_type_revision = 1} } } - }, - { - min_api_version = 17 } -) +}) + +local function test_init_no_hue_sat() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_color_device_no_hue_sat) +end test.register_coroutine_test( - "colorControl capability sent based on CurrentHue and CurrentSaturation due to ColorMode", - function() - test.socket.matter:__queue_receive( + "Set color command should send the appropriate commands", function() + test.socket.capability:__queue_receive( { - mock_device.id, - clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) + mock_color_device_no_hue_sat.id, + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } }}, } ) - local read_hue_sat = clusters.ColorControl.attributes.CurrentHue:read() - read_hue_sat:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) test.socket.matter:__expect_send( { - mock_device.id, - read_hue_sat + mock_color_device_no_hue_sat.id, + clusters.ColorControl.server.commands.MoveToColor(mock_color_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } ) test.socket.matter:__queue_receive( { - mock_device.id, - clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, 0xFE), + mock_color_device_no_hue_sat.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_color_device_no_hue_sat, 1) } ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(100) - ) - ) - test.socket.devices:__expect_send( + test.socket.matter:__queue_receive( { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + mock_color_device_no_hue_sat.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_color_device_no_hue_sat, 1, 15091) } ) test.socket.matter:__queue_receive( { - mock_device.id, - clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, 0xFE), + mock_color_device_no_hue_sat.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_color_device_no_hue_sat, 1, 21547) } ) test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(100) + mock_color_device_no_hue_sat:generate_test_message( + "main", capabilities.colorControl.hue(50) ) ) - test.socket.devices:__expect_send( - { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } - } + test.socket.capability:__expect_send( + mock_color_device_no_hue_sat:generate_test_message( + "main", capabilities.colorControl.saturation(72) + ) ) end, { - test_init = test_init_x_y_color_mode, + test_init = test_init_no_hue_sat, min_api_version = 17 } ) + + +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.ON_OFF, device_type_revision = 1}, + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE, device_type_revision = 1} + } + } + } +}) + +local function test_init_color_temp() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_color_temp) +end + test.register_coroutine_test( - "colorControl capability sent based on CurrentX and CurrentY due to ColorMode", + "doConfigure properly sets Color Temperature Light and does not attempt to switch profiles", function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) - } - ) - local read_x_y = clusters.ColorControl.attributes.CurrentX:read() - read_x_y:merge(clusters.ColorControl.attributes.CurrentY:read()) - test.socket.matter:__expect_send( - { - mock_device.id, - read_x_y - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.hue(50) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.colorControl.saturation(72) - ) - ) + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, { persist = true }) + mock_device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.LevelControl.attributes.Options:write(mock_device_color_temp, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.ColorControl.attributes.Options:write(mock_device_color_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + test_init = test_init_color_temp, + min_api_version = 17 } ) test.register_coroutine_test( - "Refresh necessary attributes", + "init creates accurate subscription for Color Temperature Light", function() - test.socket.capability:__queue_receive( - {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} - ) - local read_request = cluster_subscribe_list[1]:read(mock_device) - for i, attr in ipairs(cluster_subscribe_list) do - if i > 1 then read_request:merge(attr:read(mock_device)) end + local cluster_subscribe_list_color_temp = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds + } + local subscribe_request = cluster_subscribe_list_color_temp[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list_color_temp) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end end - test.socket.matter:__expect_send({mock_device.id, read_request}) - test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) end, { - min_api_version = 17 + test_init = test_init_color_temp, + min_api_version = 17 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua deleted file mode 100644 index 37b5ec8ca5..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ /dev/null @@ -1,804 +0,0 @@ --- Copyright © 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local clusters = require "st.matter.clusters" - -test.disable_startup_messages() - -local mock_device_onoff = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - matter_version = { - hardware = 1, - software = 1, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch - } - } - } -}) - -local mock_device_onoff_client = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch - } - } - } -}) - -local mock_device_dimmer = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 5, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch - } - } - } -}) - -local mock_device_switch_vendor_override = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-binary.yml"), - manufacturer_info = { - vendor_id = 0x109B, - product_id = 0x1001, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 1} -- OnOff PlugIn Unit - } - } - } -}) - - -local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "CLIENT", feature_map = 31}, - - }, - device_types = { - {device_type_id = 0x0105, device_type_revision = 1} -- Color Dimmer Switch - } - } - } -}) - -local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-binary.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - - }, - device_types = { - {device_type_id = 0x010F, device_type_revision = 1} -- Mounted On/Off Control - } - } - } -}) - -local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-level.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - - }, - device_types = { - {device_type_id = 0x0110, device_type_revision = 1} -- Mounted Dimmable Load Control - } - } - } -}) - -local mock_device_water_valve = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.ValveConfigurationAndControl.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2}, - }, - device_types = { - {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve - } - } - } -}) - -local mock_device_parent_client_child_server = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - } -}) - -local mock_device_parent_child_switch_types = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - } -}) - -local mock_device_parent_child_different_types = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("switch-binary.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - } - } -}) - -local mock_device_parent_child_unsupported_device_type = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0304, device_type_revision = 2} -- Pump Controller - } - } - } -}) - -local mock_device_light_level_motion = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("light-level-motion.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} - }, - device_types = { - {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light - } - }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor - } - } - } -}) - -local function test_init_parent_child_switch_types() - test.mock_device.add_test_device(mock_device_parent_child_switch_types) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_parent_child_switch_types.id, - clusters.LevelControl.attributes.Options:write(mock_device_parent_child_switch_types, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) - mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - mock_device_parent_child_switch_types:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_parent_child_switch_types.id, - parent_assigned_child_key = string.format("%d", 10) - }) -end - -local function test_init_onoff() - test.mock_device.add_test_device(mock_device_onoff) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) - mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) - mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_onoff_client() - test.mock_device.add_test_device(mock_device_onoff_client) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "doConfigure" }) - mock_device_onoff_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_parent_client_child_server() - test.mock_device.add_test_device(mock_device_parent_client_child_server) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) - mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) - mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_dimmer() - test.mock_device.add_test_device(mock_device_dimmer) - test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_dimmer.id, - clusters.LevelControl.attributes.Options:write(mock_device_dimmer, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) - mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_color_dimmer() - test.mock_device.add_test_device(mock_device_color_dimmer) - test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) - mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) - mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_switch_vendor_override() - test.mock_device.add_test_device(mock_device_switch_vendor_override) - local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_switch_vendor_override) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "added" }) - test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "init" }) - test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "doConfigure" }) - mock_device_switch_vendor_override:expect_metadata_update({ profile = "switch-binary" }) - mock_device_switch_vendor_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_mounted_on_off_control() - test.mock_device.add_test_device(mock_device_mounted_on_off_control) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_on_off_control) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mounted_on_off_control)) - end - end - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) - test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) - test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_mounted_on_off_control.id, - clusters.LevelControl.attributes.Options:write(mock_device_mounted_on_off_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) - mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_mounted_dimmable_load_control() - test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.LevelControl.attributes.MaxLevel, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_dimmable_load_control) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mounted_dimmable_load_control)) - end - end - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) - test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) - test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_mounted_dimmable_load_control.id, - clusters.LevelControl.attributes.Options:write(mock_device_mounted_dimmable_load_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) - mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_water_valve() - test.mock_device.add_test_device(mock_device_water_valve) - test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) - mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) - mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -local function test_init_parent_child_different_types() - test.mock_device.add_test_device(mock_device_parent_child_different_types) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "init" }) - test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_parent_child_different_types.id, - clusters.LevelControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - test.socket.matter:__expect_send({ - mock_device_parent_child_different_types.id, - clusters.ColorControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) - mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - mock_device_parent_child_different_types:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device_parent_child_different_types.id, - parent_assigned_child_key = string.format("%d", 10) - }) -end - -local function test_init_parent_child_unsupported_device_type() - test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) - mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) - test.socket.matter:__expect_send({ - mock_device_parent_child_unsupported_device_type.id, - clusters.LevelControl.attributes.Options:write(mock_device_parent_child_unsupported_device_type, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - mock_device_parent_child_unsupported_device_type:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_parent_child_unsupported_device_type.id, - parent_assigned_child_key = string.format("%d", 10) - }) -end - -local function test_init_light_level_motion() - test.mock_device.add_test_device(mock_device_light_level_motion) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_light_level_motion) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "added" }) - test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) - test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) - test.socket.matter:__expect_send({ - mock_device_light_level_motion.id, - clusters.LevelControl.attributes.Options:write(mock_device_light_level_motion, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) - }) - mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) - mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -end - -test.register_coroutine_test( - "Test profile change on init for onoff parent cluster as server", - function() - end, - { - test_init = test_init_onoff, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for dimmer parent cluster as server", - function() - end, - { - test_init = test_init_dimmer, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for color dimmer parent cluster as server", - function() - end, - { - test_init = test_init_color_dimmer, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for onoff parent cluster as client", - function() - end, - { - test_init = test_init_onoff_client, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for device with requiring the switch category as a vendor override", - function() - end, - { - test_init = test_init_switch_vendor_override, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for mounted onoff control parent cluster as server", - function() - end, - { - test_init = test_init_mounted_on_off_control, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for mounted dimmable load control parent cluster as server", - function() - end, - { - test_init = test_init_mounted_dimmable_load_control, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for water valve parent cluster as server", - function() - end, - { - test_init = test_init_water_valve, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for onoff parent cluster as client and onoff child as server", - function() - end, - { - test_init = test_init_parent_client_child_server, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for onoff device when parent and child are both server", - function() - end, - { - test_init = test_init_parent_child_switch_types, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child device attribute subscriptions when parent device has clusters that are not a superset of child device clusters", - function() - end, - { - test_init = test_init_parent_child_different_types, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child device attributes not subscribed to for unsupported device type for child device", - function() - end, - { - test_init = test_init_parent_child_unsupported_device_type, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for light with motion sensor", - function() - end, - { - test_init = test_init_light_level_motion, - min_api_version = 17 - } -) - -test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua index 338acd58c7..788a00d76e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -45,27 +45,33 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) -local cluster_subscribe_list = { - clusters.ValveConfigurationAndControl.attributes.CurrentState, - clusters.ValveConfigurationAndControl.attributes.CurrentLevel -} - local function test_init() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - -- the following subscribe is due to the init event sent by the test framework. - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Device should be added with correct subscription and profile", + function() + local cluster_subscribe_list = { + clusters.ValveConfigurationAndControl.attributes.CurrentState, + clusters.ValveConfigurationAndControl.attributes.CurrentLevel + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "water-valve-level" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.register_message_test( "Open command should send the appropriate commands", { @@ -134,16 +140,7 @@ test.register_message_test( mock_device.id, clusters.ValveConfigurationAndControl.server.commands.Open(mock_device, 1, nil, 25) } - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set level command should send the appropriate commands", - { + }, { channel = "capability", direction = "receive", @@ -183,13 +180,6 @@ test.register_message_test( message = mock_device:generate_test_message("main", capabilities.valve.valve.closed()) }, }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Current state reports should generate appropriate events", { { channel = "matter", @@ -204,15 +194,6 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.valve.valve.open()) }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Current state reports should generate appropriate events", - { { channel = "matter", direction = "receive", diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua index 7976b27a4a..5b1b569b25 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua @@ -145,8 +145,6 @@ local function test_init_mock_3switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch) - test.socket.device_lifecycle:__queue_receive({ mock_3switch.id, "added" }) - test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) @@ -161,8 +159,6 @@ local function test_init_mock_2switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_2switch) - test.socket.device_lifecycle:__queue_receive({ mock_2switch.id, "added" }) - test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.mock_device.add_test_device(mock_2switch) @@ -176,8 +172,6 @@ local function test_init_mock_3switch_non_sequential() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch_non_sequential) - test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "added" }) - test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch_non_sequential) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua deleted file mode 100644 index 8102c61865..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ /dev/null @@ -1,740 +0,0 @@ --- Copyright © 2024 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" - -test.disable_startup_messages() - -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 0x01 - -local parent_ep = 10 -local child1_ep = 20 -local child2_ep = 30 - -local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - matter_version = { - hardware = 1, - software = 1, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - endpoint_id = child1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - endpoint_id = child2_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - } -}) - -local child1_ep_non_sequential = 50 -local child2_ep_non_sequential = 30 -local child3_ep_non_sequential = 40 - -local mock_device_parent_child_endpoints_non_sequential = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x1321, - product_id = 0x000C, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - endpoint_id = child1_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - endpoint_id = child2_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = child3_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - } -}) - -local child_profiles = { - [child1_ep] = t_utils.get_profile_definition("light-level.yml"), - [child2_ep] = t_utils.get_profile_definition("light-color-level.yml"), -} - -local mock_children = {} -for i, endpoint in ipairs(mock_device.endpoints) do - if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init() - test.mock_device.add_test_device(mock_device) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device:expect_metadata_update({ profile = "light-binary" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - for _, child in pairs(mock_children) do - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) -end - -local child_profiles_non_sequential = { - [child1_ep_non_sequential] = t_utils.get_profile_definition("light-level.yml"), - [child2_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), - [child3_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), -} - -local mock_children_non_sequential = {} -for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endpoints) do - if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles_non_sequential[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device_parent_child_endpoints_non_sequential.id, endpoint.endpoint_id), - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init_parent_child_endpoints_non_sequential() - local unsup_mock_device = mock_device_parent_child_endpoints_non_sequential - - test.mock_device.add_test_device(unsup_mock_device) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(unsup_mock_device)) - end - end - - test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) - test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) - test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({unsup_mock_device.id, clusters.ColorControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - - unsup_mock_device:expect_metadata_update({ profile = "switch-binary" }) - unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - for _, child in pairs(mock_children_non_sequential) do - test.mock_device.add_test_device(child) - end - - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = unsup_mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) - }) - - -- switch-binary will be selected as an overridden child device profile - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "switch-binary", - parent_device_id = unsup_mock_device.id, - parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) - }) - - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 4", - profile = "light-level", - parent_device_id = unsup_mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) - }) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Parent device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, parent_ep) - }, - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "First child device: switch capability switch should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child1_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child1_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Second child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child2_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Current level reports should generate appropriate events", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child1_ep, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child2_ep, 556) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "X and Y color values should report hue and saturation once both have been received", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min and max level attributes set capability constraint for child devices", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child1_ep, 1) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child1_ep, 254) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child2_ep, 127) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child2_ep, 203) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min and max color temp attributes set capability constraint for child devices", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, child2_ep, 153) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, child2_ep, 555) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child devices are created in order of their endpoints", - function() - end, - { - test_init = test_init_parent_child_endpoints_non_sequential, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test info changed event with matter_version update", - function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 - mock_children[child1_ep]:expect_metadata_update({ profile = "light-level" }) - mock_children[child2_ep]:expect_metadata_update({ profile = "light-color-level" }) - mock_device:expect_metadata_update({ profile = "light-binary" }) - end, - { - min_api_version = 17 - } -) - -test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua deleted file mode 100644 index 9beed1805e..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ /dev/null @@ -1,720 +0,0 @@ --- Copyright © 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" - -test.disable_startup_messages() - -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 0x01 - -local parent_ep = 10 -local child1_ep = 20 -local child2_ep = 30 - -local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - endpoint_id = child1_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - endpoint_id = child2_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - } -}) - -local child1_ep_non_sequential = 50 -local child2_ep_non_sequential = 30 -local child3_ep_non_sequential = 40 - -local mock_device_parent_child_endpoints_non_sequential = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x1321, - product_id = 0x000C, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - endpoint_id = child1_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - endpoint_id = child2_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, - }, - device_types = { - {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - { - endpoint_id = child3_ep_non_sequential, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - } -}) - -local child_profiles = { - [child1_ep] = t_utils.get_profile_definition("light-level.yml"), - [child2_ep] = t_utils.get_profile_definition("light-color-level.yml"), -} - -local mock_children = {} -for i, endpoint in ipairs(mock_device.endpoints) do - if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init() - test.mock_device.add_test_device(mock_device) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device:expect_metadata_update({ profile = "light-binary" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - for _, child in pairs(mock_children) do - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) -end - -local child_profiles_non_sequential = { - [child1_ep_non_sequential] = t_utils.get_profile_definition("light-level.yml"), - [child2_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), - [child3_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), -} - -local mock_children_non_sequential = {} -for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endpoints) do - if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then - local child_data = { - profile = child_profiles_non_sequential[endpoint.endpoint_id], - device_network_id = string.format("%s:%d", mock_device_parent_child_endpoints_non_sequential.id, endpoint.endpoint_id), - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, - parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) - } - mock_children_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init_parent_child_endpoints_non_sequential() - test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.ColorControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "switch-binary" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - for _, child in pairs(mock_children_non_sequential) do - test.mock_device.add_test_device(child) - end - - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, - parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) - }) - - -- switch-binary will be selected as an overridden child device profile - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "switch-binary", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, - parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) - }) - - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 4", - profile = "light-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, - parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) - }) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Parent device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, parent_ep) - }, - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "First child device: switch capability switch should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child1_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child1_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Second child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child2_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Current level reports should generate appropriate events", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child1_ep, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child2_ep, 556) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "X and Y color values should report hue and saturation once both have been received", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Set color command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min and max level attributes set capability constraint for child devices", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child1_ep, 1) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child1_ep, 254) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child2_ep, 127) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child2_ep, 203) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) - } - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Min and max color temp attributes set capability constraint for child devices", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, child2_ep, 153) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, child2_ep, 555) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child devices are created in order of their endpoints", - function() - end, - { - test_init = test_init_parent_child_endpoints_non_sequential, - min_api_version = 17 - } -) - -test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua index 8d05070efc..a92c547ac7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua @@ -62,12 +62,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 60, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 60, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -78,12 +86,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 271, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 271, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -94,12 +110,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.UP, 151, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.UP, 151, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, } }, @@ -120,12 +144,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 64, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 64, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -136,12 +168,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.DOWN, 127, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.DOWN, 127, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -152,12 +192,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { channel = "matter", direction = "send", message = { mock_device_color_temp.id, - clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 254, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 254, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, } }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 82a8d4d641..b7e94797f6 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -185,7 +185,6 @@ local function configure_buttons() local component = "F" .. key if key == 1 then component = "main" end test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.button.pushed({state_change = false}))) end end diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua index 9108d96096..a678e85499 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua @@ -198,6 +198,79 @@ local function initialize_subscribe_request(mock_device, subscribed_attributes) end +local function test_init_modular_fingerprint() + test.mock_device.add_test_device(mock_device_modular) + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "init" }) + local subscribe_request = initialize_subscribe_request(mock_device_modular, { + [clusters.Thermostat.ID] = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + }, + [clusters.TemperatureMeasurement.ID] = { + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + }, + }) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) +end + +test.register_coroutine_test( +"Component-capability update without profile ID update should cause re-subscribe in infoChanged handler", function() + local subscribe_request = initialize_subscribe_request(mock_device_modular, { + [clusters.Thermostat.ID] = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + }, + [clusters.TemperatureMeasurement.ID] = { + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + }, + [clusters.FanControl.ID] = { + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + }, + }) + local expected_metadata_modular = { + optional_component_capabilities={{"main", {"fanMode"}}}, + profile="thermostat-modular", + } + local updated_device_profile = t_utils.get_profile_definition("thermostat-modular.yml", + {enabled_optional_capabilities = expected_metadata_modular.optional_component_capabilities} + ) + updated_device_profile.id = "00000000-1111-2222-3333-000000000003" + test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) + end, + { test_init = test_init_modular_fingerprint } +) + +test.register_coroutine_test( + "No component-capability update and no profile ID update should not cause a re-subscribe in infoChanged handler", function() + -- simulate no actual change + test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({})) + end, + { test_init = function() test.mock_device.add_test_device(mock_device_modular) end } +) + +local function initialize_subscribe_request(mock_device, subscribed_attributes) + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in pairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device) + else + subscribe_request:merge(attribute:subscribe(mock_device)) + end + end + end + return subscribe_request +end + + local function test_init_modular_fingerprint() test.mock_device.add_test_device(mock_device_modular) test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "init" }) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 531dc8c36f..34406cf01c 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -261,3 +261,8 @@ matterGeneric: deviceTypes: - id: 0x0202 # Window Covering deviceProfileName: window-covering + - id: "closure" + deviceLabel: Matter Closure + deviceTypes: + - id: 0x0230 # Closure + deviceProfileName: covering diff --git a/drivers/SmartThings/matter-window-covering/profiles/covering.yml b/drivers/SmartThings/matter-window-covering/profiles/covering.yml new file mode 100644 index 0000000000..11e2319a5a --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/covering.yml @@ -0,0 +1,56 @@ +name: covering +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadeLevel + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +- id: windowShade1 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade2 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade3 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade4 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/door.yml b/drivers/SmartThings/matter-window-covering/profiles/door.yml new file mode 100644 index 0000000000..bd19c2dc8f --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/door.yml @@ -0,0 +1,56 @@ +name: door +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Door +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml b/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml new file mode 100644 index 0000000000..a75ae63580 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml @@ -0,0 +1,56 @@ +name: garage-door +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: GarageDoor +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/gate.yml b/drivers/SmartThings/matter-window-covering/profiles/gate.yml new file mode 100644 index 0000000000..8cc49b425f --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/gate.yml @@ -0,0 +1,56 @@ +name: gate +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Door +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua new file mode 100644 index 0000000000..76adc03375 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua @@ -0,0 +1,97 @@ +local cluster_base = require "st.matter.cluster_base" +local ClosureControlServerAttributes = require "embedded_clusters.ClosureControl.server.attributes" +local ClosureControlServerCommands = require "embedded_clusters.ClosureControl.server.commands" +local ClosureControlTypes = require "embedded_clusters.ClosureControl.types" + +local ClosureControl = {} + +ClosureControl.ID = 0x0104 +ClosureControl.NAME = "ClosureControl" +ClosureControl.server = {} +ClosureControl.client = {} +ClosureControl.server.attributes = ClosureControlServerAttributes:set_parent_cluster(ClosureControl) +ClosureControl.server.commands = ClosureControlServerCommands:set_parent_cluster(ClosureControl) +ClosureControl.types = ClosureControlTypes + +function ClosureControl:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CountdownTime", + [0x0001] = "MainState", + [0x0002] = "CurrentErrorList", + [0x0003] = "OverallCurrentState", + [0x0004] = "OverallTargetState", + [0x0005] = "LatchControlModes", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ClosureControl:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "Stop", + [0x0001] = "MoveTo", + [0x0002] = "Calibrate", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + + +ClosureControl.attribute_direction_map = { + ["CountdownTime"] = "server", + ["MainState"] = "server", + ["CurrentErrorList"] = "server", + ["OverallCurrentState"] = "server", + ["OverallTargetState"] = "server", + ["LatchControlModes"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +ClosureControl.command_direction_map = { + ["Stop"] = "server", + ["MoveTo"] = "server", + ["Calibrate"] = "server", +} + +ClosureControl.FeatureMap = ClosureControl.types.Feature + +function ClosureControl.are_features_supported(feature, feature_map) + if (ClosureControl.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ClosureControl.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ClosureControl.NAME)) + end + return ClosureControl[direction].attributes[key] +end +ClosureControl.attributes = {} +setmetatable(ClosureControl.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ClosureControl.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ClosureControl.NAME)) + end + return ClosureControl[direction].commands[key] +end +ClosureControl.commands = {} +setmetatable(ClosureControl.commands, command_helper_mt) + +setmetatable(ClosureControl, {__index = cluster_base}) + +return ClosureControl diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..f86ac60dbe --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..7f6827b026 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.lua new file mode 100644 index 0000000000..b6e4f861b6 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CountdownTime = { + ID = 0x0000, + NAME = "CountdownTime", + base_type = require "st.matter.data_types.Uint32", +} + +function CountdownTime:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CountdownTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CountdownTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CountdownTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CountdownTime:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CountdownTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CountdownTime, {__call = CountdownTime.new_value, __index = CountdownTime.base_type}) +return CountdownTime diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.lua new file mode 100644 index 0000000000..21076e7e5d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentErrorList = { + ID = 0x0002, + NAME = "CurrentErrorList", + base_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.ClosureControl.types.ClosureErrorEnum", +} + +function CurrentErrorList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, CurrentErrorList.element_type) + end +end + +function CurrentErrorList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CurrentErrorList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentErrorList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentErrorList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentErrorList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CurrentErrorList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CurrentErrorList, {__call = CurrentErrorList.new_value, __index = CurrentErrorList.base_type}) +return CurrentErrorList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.lua new file mode 100644 index 0000000000..d62831b4f3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local LatchControlModes = { + ID = 0x0005, + NAME = "LatchControlModes", + base_type = require "embedded_clusters.ClosureControl.types.LatchControlModesBitmap", +} + +function LatchControlModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LatchControlModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LatchControlModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LatchControlModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LatchControlModes:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function LatchControlModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LatchControlModes, {__call = LatchControlModes.new_value, __index = LatchControlModes.base_type}) +return LatchControlModes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.lua new file mode 100644 index 0000000000..5f1bbc06e2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MainState = { + ID = 0x0001, + NAME = "MainState", + base_type = require "embedded_clusters.ClosureControl.types.MainStateEnum", +} + +function MainState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MainState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function MainState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function MainState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MainState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MainState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MainState, {__call = MainState.new_value, __index = MainState.base_type}) +return MainState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.lua new file mode 100644 index 0000000000..f93477ec47 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local OverallCurrentState = { + ID = 0x0003, + NAME = "OverallCurrentState", + base_type = require "embedded_clusters.ClosureControl.types.OverallCurrentStateStruct", +} + +function OverallCurrentState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function OverallCurrentState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallCurrentState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallCurrentState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function OverallCurrentState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function OverallCurrentState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(OverallCurrentState, {__call = OverallCurrentState.new_value, __index = OverallCurrentState.base_type}) +return OverallCurrentState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.lua new file mode 100644 index 0000000000..6c8cbb9ee3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local OverallTargetState = { + ID = 0x0004, + NAME = "OverallTargetState", + base_type = require "embedded_clusters.ClosureControl.types.OverallTargetStateStruct", +} + +function OverallTargetState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function OverallTargetState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallTargetState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallTargetState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function OverallTargetState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function OverallTargetState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(OverallTargetState, {__call = OverallTargetState.new_value, __index = OverallTargetState.base_type}) +return OverallTargetState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua new file mode 100644 index 0000000000..0c71152760 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureControl.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureControlServerAttributes = {} + +function ClosureControlServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureControlServerAttributes, attr_mt) + +return ClosureControlServerAttributes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua new file mode 100644 index 0000000000..1a2931c0ad --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua @@ -0,0 +1,91 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Calibrate = {} + +Calibrate.NAME = "Calibrate" +Calibrate.ID = 0x0002 +Calibrate.field_defs = { +} + +function Calibrate:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function Calibrate:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Calibrate, + __tostring = Calibrate.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function Calibrate:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Calibrate:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function Calibrate:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Calibrate, {__call = Calibrate.init}) + +return Calibrate diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua new file mode 100644 index 0000000000..80c1bca52d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MoveTo = {} + +MoveTo.NAME = "MoveTo" +MoveTo.ID = 0x0001 +MoveTo.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.TargetPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function MoveTo:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function MoveTo:init(device, endpoint_id, position, latch, speed) + local out = {} + local args = {position, latch, speed} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = MoveTo, + __tostring = MoveTo.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function MoveTo:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MoveTo:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function MoveTo:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MoveTo, {__call = MoveTo.init}) + +return MoveTo diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua new file mode 100644 index 0000000000..9ac41ba122 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua @@ -0,0 +1,90 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Stop = {} + +Stop.NAME = "Stop" +Stop.ID = 0x0000 +Stop.field_defs = { +} + +function Stop:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function Stop:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Stop, + __tostring = Stop.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function Stop:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Stop:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function Stop:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Stop, {__call = Stop.init}) + +return Stop diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua new file mode 100644 index 0000000000..1125ef4296 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua @@ -0,0 +1,19 @@ +local command_mt = {} +command_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureControl.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureControlServerCommands = {} + +function ClosureControlServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureControlServerCommands, command_mt) + +return ClosureControlServerCommands diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua new file mode 100644 index 0000000000..5e5a09da56 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ClosureErrorEnum = {} +local new_mt = UintABC.new_mt({NAME = "ClosureErrorEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.PHYSICALLY_BLOCKED] = "PHYSICALLY_BLOCKED", + [self.BLOCKED_BY_SENSOR] = "BLOCKED_BY_SENSOR", + [self.TEMPERATURE_LIMITED] = "TEMPERATURE_LIMITED", + [self.MAINTENANCE_REQUIRED] = "MAINTENANCE_REQUIRED", + [self.INTERNAL_INTERFERENCE] = "INTERNAL_INTERFERENCE", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.PHYSICALLY_BLOCKED = 0x00 +new_mt.__index.BLOCKED_BY_SENSOR = 0x01 +new_mt.__index.TEMPERATURE_LIMITED = 0x02 +new_mt.__index.MAINTENANCE_REQUIRED = 0x03 +new_mt.__index.INTERNAL_INTERFERENCE = 0x04 + +ClosureErrorEnum.PHYSICALLY_BLOCKED = 0x00 +ClosureErrorEnum.BLOCKED_BY_SENSOR = 0x01 +ClosureErrorEnum.TEMPERATURE_LIMITED = 0x02 +ClosureErrorEnum.MAINTENANCE_REQUIRED = 0x03 +ClosureErrorEnum.INTERNAL_INTERFERENCE = 0x04 + +ClosureErrorEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ClosureErrorEnum, new_mt) + +return ClosureErrorEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua new file mode 100644 index 0000000000..a5fee86d89 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua @@ -0,0 +1,39 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local CurrentPositionEnum = {} +local new_mt = UintABC.new_mt({NAME = "CurrentPositionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.FULLY_CLOSED] = "FULLY_CLOSED", + [self.FULLY_OPENED] = "FULLY_OPENED", + [self.PARTIALLY_OPENED] = "PARTIALLY_OPENED", + [self.OPENED_FOR_PEDESTRIAN] = "OPENED_FOR_PEDESTRIAN", + [self.OPENED_FOR_VENTILATION] = "OPENED_FOR_VENTILATION", + [self.OPENED_AT_SIGNATURE] = "OPENED_AT_SIGNATURE", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.FULLY_CLOSED = 0x00 +new_mt.__index.FULLY_OPENED = 0x01 +new_mt.__index.PARTIALLY_OPENED = 0x02 +new_mt.__index.OPENED_FOR_PEDESTRIAN = 0x03 +new_mt.__index.OPENED_FOR_VENTILATION = 0x04 +new_mt.__index.OPENED_AT_SIGNATURE = 0x05 + +CurrentPositionEnum.FULLY_CLOSED = 0x00 +CurrentPositionEnum.FULLY_OPENED = 0x01 +CurrentPositionEnum.PARTIALLY_OPENED = 0x02 +CurrentPositionEnum.OPENED_FOR_PEDESTRIAN = 0x03 +CurrentPositionEnum.OPENED_FOR_VENTILATION = 0x04 +CurrentPositionEnum.OPENED_AT_SIGNATURE = 0x05 + +CurrentPositionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(CurrentPositionEnum, new_mt) + +return CurrentPositionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua new file mode 100644 index 0000000000..e41485dbe7 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua @@ -0,0 +1,228 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.POSITIONING = 0x0001 +Feature.MOTION_LATCHING = 0x0002 +Feature.INSTANTANEOUS = 0x0004 +Feature.SPEED = 0x0008 +Feature.VENTILATION = 0x0010 +Feature.PEDESTRIAN = 0x0020 +Feature.CALIBRATION = 0x0040 +Feature.PROTECTION = 0x0080 +Feature.MANUALLY_OPERABLE = 0x0100 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + POSITIONING = 0x0001, + MOTION_LATCHING = 0x0002, + INSTANTANEOUS = 0x0004, + SPEED = 0x0008, + VENTILATION = 0x0010, + PEDESTRIAN = 0x0020, + CALIBRATION = 0x0040, + PROTECTION = 0x0080, + MANUALLY_OPERABLE = 0x0100, +} + +Feature.is_positioning_set = function(self) + return (self.value & self.POSITIONING) ~= 0 +end + +Feature.set_positioning = function(self) + if self.value ~= nil then + self.value = self.value | self.POSITIONING + else + self.value = self.POSITIONING + end +end + +Feature.unset_positioning = function(self) + self.value = self.value & (~self.POSITIONING & self.BASE_MASK) +end +Feature.is_motion_latching_set = function(self) + return (self.value & self.MOTION_LATCHING) ~= 0 +end + +Feature.set_motion_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.MOTION_LATCHING + else + self.value = self.MOTION_LATCHING + end +end + +Feature.unset_motion_latching = function(self) + self.value = self.value & (~self.MOTION_LATCHING & self.BASE_MASK) +end + +Feature.is_instantaneous_set = function(self) + return (self.value & self.INSTANTANEOUS) ~= 0 +end + +Feature.set_instantaneous = function(self) + if self.value ~= nil then + self.value = self.value | self.INSTANTANEOUS + else + self.value = self.INSTANTANEOUS + end +end + +Feature.unset_instantaneous = function(self) + self.value = self.value & (~self.INSTANTANEOUS & self.BASE_MASK) +end + +Feature.is_speed_set = function(self) + return (self.value & self.SPEED) ~= 0 +end + +Feature.set_speed = function(self) + if self.value ~= nil then + self.value = self.value | self.SPEED + else + self.value = self.SPEED + end +end + +Feature.unset_speed = function(self) + self.value = self.value & (~self.SPEED & self.BASE_MASK) +end + +Feature.is_ventilation_set = function(self) + return (self.value & self.VENTILATION) ~= 0 +end + +Feature.set_ventilation = function(self) + if self.value ~= nil then + self.value = self.value | self.VENTILATION + else + self.value = self.VENTILATION + end +end + +Feature.unset_ventilation = function(self) + self.value = self.value & (~self.VENTILATION & self.BASE_MASK) +end + +Feature.is_pedestrian_set = function(self) + return (self.value & self.PEDESTRIAN) ~= 0 +end + +Feature.set_pedestrian = function(self) + if self.value ~= nil then + self.value = self.value | self.PEDESTRIAN + else + self.value = self.PEDESTRIAN + end +end + +Feature.unset_pedestrian = function(self) + self.value = self.value & (~self.PEDESTRIAN & self.BASE_MASK) +end + +Feature.is_calibration_set = function(self) + return (self.value & self.CALIBRATION) ~= 0 +end + +Feature.set_calibration = function(self) + if self.value ~= nil then + self.value = self.value | self.CALIBRATION + else + self.value = self.CALIBRATION + end +end + +Feature.unset_calibration = function(self) + self.value = self.value & (~self.CALIBRATION & self.BASE_MASK) +end + +Feature.is_protection_set = function(self) + return (self.value & self.PROTECTION) ~= 0 +end + +Feature.set_protection = function(self) + if self.value ~= nil then + self.value = self.value | self.PROTECTION + else + self.value = self.PROTECTION + end +end + +Feature.unset_protection = function(self) + self.value = self.value & (~self.PROTECTION & self.BASE_MASK) +end + +Feature.is_manually_operable_set = function(self) + return (self.value & self.MANUALLY_OPERABLE) ~= 0 +end + +Feature.set_manually_operable = function(self) + if self.value ~= nil then + self.value = self.value | self.MANUALLY_OPERABLE + else + self.value = self.MANUALLY_OPERABLE + end +end + +Feature.unset_manually_operable = function(self) + self.value = self.value & (~self.MANUALLY_OPERABLE & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.POSITIONING | + Feature.MOTION_LATCHING | + Feature.INSTANTANEOUS | + Feature.SPEED | + Feature.VENTILATION | + Feature.PEDESTRIAN | + Feature.CALIBRATION | + Feature.PROTECTION | + Feature.MANUALLY_OPERABLE + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_positioning_set = Feature.is_positioning_set, + set_positioning = Feature.set_positioning, + unset_positioning = Feature.unset_positioning, + is_motion_latching_set = Feature.is_motion_latching_set, + set_motion_latching = Feature.set_motion_latching, + unset_motion_latching = Feature.unset_motion_latching, + is_instantaneous_set = Feature.is_instantaneous_set, + set_instantaneous = Feature.set_instantaneous, + unset_instantaneous = Feature.unset_instantaneous, + is_speed_set = Feature.is_speed_set, + set_speed = Feature.set_speed, + unset_speed = Feature.unset_speed, + is_ventilation_set = Feature.is_ventilation_set, + set_ventilation = Feature.set_ventilation, + unset_ventilation = Feature.unset_ventilation, + is_pedestrian_set = Feature.is_pedestrian_set, + set_pedestrian = Feature.set_pedestrian, + unset_pedestrian = Feature.unset_pedestrian, + is_calibration_set = Feature.is_calibration_set, + set_calibration = Feature.set_calibration, + unset_calibration = Feature.unset_calibration, + is_protection_set = Feature.is_protection_set, + set_protection = Feature.set_protection, + unset_protection = Feature.unset_protection, + is_manually_operable_set = Feature.is_manually_operable_set, + set_manually_operable = Feature.set_manually_operable, + unset_manually_operable = Feature.unset_manually_operable, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua new file mode 100644 index 0000000000..ba6188e4a3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua @@ -0,0 +1,64 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LatchControlModesBitmap = {} +local new_mt = UintABC.new_mt({NAME = "LatchControlModesBitmap", ID = data_types.name_to_id_map["Uint8"]}, 1) + +LatchControlModesBitmap.BASE_MASK = 0xFFFF +LatchControlModesBitmap.REMOTE_LATCHING = 0x0001 +LatchControlModesBitmap.REMOTE_UNLATCHING = 0x0002 + +LatchControlModesBitmap.mask_fields = { + BASE_MASK = 0xFFFF, + REMOTE_LATCHING = 0x0001, + REMOTE_UNLATCHING = 0x0002, +} + +LatchControlModesBitmap.is_remote_latching_set = function(self) + return (self.value & self.REMOTE_LATCHING) ~= 0 +end + +LatchControlModesBitmap.set_remote_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.REMOTE_LATCHING + else + self.value = self.REMOTE_LATCHING + end +end + +LatchControlModesBitmap.unset_remote_latching = function(self) + self.value = self.value & (~self.REMOTE_LATCHING & self.BASE_MASK) +end + +LatchControlModesBitmap.is_remote_unlatching_set = function(self) + return (self.value & self.REMOTE_UNLATCHING) ~= 0 +end + +LatchControlModesBitmap.set_remote_unlatching = function(self) + if self.value ~= nil then + self.value = self.value | self.REMOTE_UNLATCHING + else + self.value = self.REMOTE_UNLATCHING + end +end + +LatchControlModesBitmap.unset_remote_unlatching = function(self) + self.value = self.value & (~self.REMOTE_UNLATCHING & self.BASE_MASK) +end + +LatchControlModesBitmap.mask_methods = { + is_remote_latching_set = LatchControlModesBitmap.is_remote_latching_set, + set_remote_latching = LatchControlModesBitmap.set_remote_latching, + unset_remote_latching = LatchControlModesBitmap.unset_remote_latching, + is_remote_unlatching_set = LatchControlModesBitmap.is_remote_unlatching_set, + set_remote_unlatching = LatchControlModesBitmap.set_remote_unlatching, + unset_remote_unlatching = LatchControlModesBitmap.unset_remote_unlatching, +} + +LatchControlModesBitmap.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LatchControlModesBitmap, new_mt) + +return LatchControlModesBitmap diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua new file mode 100644 index 0000000000..916e27dda0 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua @@ -0,0 +1,45 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MainStateEnum = {} +local new_mt = UintABC.new_mt({NAME = "MainStateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.STOPPED] = "STOPPED", + [self.MOVING] = "MOVING", + [self.WAITING_FOR_MOTION] = "WAITING_FOR_MOTION", + [self.ERROR] = "ERROR", + [self.CALIBRATING] = "CALIBRATING", + [self.PROTECTED] = "PROTECTED", + [self.DISENGAGED] = "DISENGAGED", + [self.SETUP_REQUIRED] = "SETUP_REQUIRED", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.STOPPED = 0x00 +new_mt.__index.MOVING = 0x01 +new_mt.__index.WAITING_FOR_MOTION = 0x02 +new_mt.__index.ERROR = 0x03 +new_mt.__index.CALIBRATING = 0x04 +new_mt.__index.PROTECTED = 0x05 +new_mt.__index.DISENGAGED = 0x06 +new_mt.__index.SETUP_REQUIRED = 0x07 + +MainStateEnum.STOPPED = 0x00 +MainStateEnum.MOVING = 0x01 +MainStateEnum.WAITING_FOR_MOTION = 0x02 +MainStateEnum.ERROR = 0x03 +MainStateEnum.CALIBRATING = 0x04 +MainStateEnum.PROTECTED = 0x05 +MainStateEnum.DISENGAGED = 0x06 +MainStateEnum.SETUP_REQUIRED = 0x07 + +MainStateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MainStateEnum, new_mt) + +return MainStateEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua new file mode 100644 index 0000000000..41af9d66db --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua @@ -0,0 +1,91 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local OverallCurrentStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "OverallCurrentStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +OverallCurrentStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.CurrentPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, + { + name = "secure_state", + field_id = 3, + is_nullable = true, + is_optional = false, + data_type = require "st.matter.data_types.Boolean", + }, +} + +OverallCurrentStateStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +OverallCurrentStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = OverallCurrentStateStruct.init +new_mt.__index.serialize = OverallCurrentStateStruct.serialize + +OverallCurrentStateStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(OverallCurrentStateStruct, new_mt) + +return OverallCurrentStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua new file mode 100644 index 0000000000..eac6492815 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local OverallTargetStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "OverallTargetStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +OverallTargetStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.TargetPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +OverallTargetStateStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +OverallTargetStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = OverallTargetStateStruct.init +new_mt.__index.serialize = OverallTargetStateStruct.serialize + +OverallTargetStateStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(OverallTargetStateStruct, new_mt) + +return OverallTargetStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua new file mode 100644 index 0000000000..b7a6122863 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local TargetPositionEnum = {} +local new_mt = UintABC.new_mt({NAME = "TargetPositionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.MOVE_TO_FULLY_CLOSED] = "MOVE_TO_FULLY_CLOSED", + [self.MOVE_TO_FULLY_OPEN] = "MOVE_TO_FULLY_OPEN", + [self.MOVE_TO_PEDESTRIAN_POSITION] = "MOVE_TO_PEDESTRIAN_POSITION", + [self.MOVE_TO_VENTILATION_POSITION] = "MOVE_TO_VENTILATION_POSITION", + [self.MOVE_TO_SIGNATURE_POSITION] = "MOVE_TO_SIGNATURE_POSITION", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.MOVE_TO_FULLY_CLOSED = 0x00 +new_mt.__index.MOVE_TO_FULLY_OPEN = 0x01 +new_mt.__index.MOVE_TO_PEDESTRIAN_POSITION = 0x02 +new_mt.__index.MOVE_TO_VENTILATION_POSITION = 0x03 +new_mt.__index.MOVE_TO_SIGNATURE_POSITION = 0x04 + +TargetPositionEnum.MOVE_TO_FULLY_CLOSED = 0x00 +TargetPositionEnum.MOVE_TO_FULLY_OPEN = 0x01 +TargetPositionEnum.MOVE_TO_PEDESTRIAN_POSITION = 0x02 +TargetPositionEnum.MOVE_TO_VENTILATION_POSITION = 0x03 +TargetPositionEnum.MOVE_TO_SIGNATURE_POSITION = 0x04 + +TargetPositionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(TargetPositionEnum, new_mt) + +return TargetPositionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua new file mode 100644 index 0000000000..6531f734a3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua @@ -0,0 +1,10 @@ +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.ClosureControl.types." .. key) +end + +local ClosureControlTypes = {} + +setmetatable(ClosureControlTypes, types_mt) + +return ClosureControlTypes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua new file mode 100644 index 0000000000..eb2f4cdae2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua @@ -0,0 +1,106 @@ +local cluster_base = require "st.matter.cluster_base" +local ClosureDimensionServerAttributes = require "embedded_clusters.ClosureDimension.server.attributes" +local ClosureDimensionServerCommands = require "embedded_clusters.ClosureDimension.server.commands" +local ClosureDimensionTypes = require "embedded_clusters.ClosureDimension.types" + +local ClosureDimension = {} + +ClosureDimension.ID = 0x0105 +ClosureDimension.NAME = "ClosureDimension" +ClosureDimension.server = {} +ClosureDimension.client = {} +ClosureDimension.server.attributes = ClosureDimensionServerAttributes:set_parent_cluster(ClosureDimension) +ClosureDimension.server.commands = ClosureDimensionServerCommands:set_parent_cluster(ClosureDimension) +ClosureDimension.types = ClosureDimensionTypes + +function ClosureDimension:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CurrentState", + [0x0001] = "TargetState", + [0x0002] = "Resolution", + [0x0003] = "StepValue", + [0x0004] = "Unit", + [0x0005] = "UnitRange", + [0x0006] = "LimitRange", + [0x0007] = "TranslationDirection", + [0x0008] = "RotationAxis", + [0x0009] = "Overflow", + [0x000A] = "ModulationType", + [0x000B] = "LatchControlModes", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ClosureDimension:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "SetTarget", + [0x0001] = "Step", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ClosureDimension.attribute_direction_map = { + ["CurrentState"] = "server", + ["TargetState"] = "server", + ["Resolution"] = "server", + ["StepValue"] = "server", + ["Unit"] = "server", + ["UnitRange"] = "server", + ["LimitRange"] = "server", + ["TranslationDirection"] = "server", + ["RotationAxis"] = "server", + ["Overflow"] = "server", + ["ModulationType"] = "server", + ["LatchControlModes"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +ClosureDimension.command_direction_map = { + ["SetTarget"] = "server", + ["Step"] = "server", +} + +ClosureDimension.FeatureMap = ClosureDimension.types.Feature + +function ClosureDimension.are_features_supported(feature, feature_map) + if (ClosureDimension.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ClosureDimension.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ClosureDimension.NAME)) + end + return ClosureDimension[direction].attributes[key] +end +ClosureDimension.attributes = {} +setmetatable(ClosureDimension.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ClosureDimension.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ClosureDimension.NAME)) + end + return ClosureDimension[direction].commands[key] +end +ClosureDimension.commands = {} +setmetatable(ClosureDimension.commands, command_helper_mt) + +setmetatable(ClosureDimension, {__index = cluster_base}) + +return ClosureDimension diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/CurrentState.lua new file mode 100644 index 0000000000..cd0566b774 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/CurrentState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentState = { + ID = 0x0000, + NAME = "CurrentState", + base_type = require "embedded_clusters.ClosureDimension.types.DimensionStateStruct", +} + +function CurrentState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CurrentState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CurrentState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CurrentState, {__call = CurrentState.new_value, __index = CurrentState.base_type}) +return CurrentState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.lua new file mode 100644 index 0000000000..8f9b357bf8 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local LimitRange = { + ID = 0x0006, + NAME = "LimitRange", + base_type = require "embedded_clusters.ClosureDimension.types.RangePercent100thsStruct", +} + +function LimitRange:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LimitRange:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LimitRange:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LimitRange:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LimitRange:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function LimitRange:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LimitRange, {__call = LimitRange.new_value, __index = LimitRange.base_type}) +return LimitRange diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.lua new file mode 100644 index 0000000000..c6958a20bb --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local StepValue = { + ID = 0x0003, + NAME = "StepValue", + base_type = require "st.matter.data_types.Uint16", +} + +function StepValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function StepValue:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function StepValue:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function StepValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function StepValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function StepValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(StepValue, {__call = StepValue.new_value, __index = StepValue.base_type}) +return StepValue diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.lua new file mode 100644 index 0000000000..fa242ca507 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local TargetState = { + ID = 0x0001, + NAME = "TargetState", + base_type = require "embedded_clusters.ClosureDimension.types.DimensionStateStruct", +} + +function TargetState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function TargetState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function TargetState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function TargetState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function TargetState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function TargetState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(TargetState, {__call = TargetState.new_value, __index = TargetState.base_type}) +return TargetState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua new file mode 100644 index 0000000000..8728e92157 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureDimension.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureDimensionServerAttributes = {} + +function ClosureDimensionServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureDimensionServerAttributes, attr_mt) + +return ClosureDimensionServerAttributes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua new file mode 100644 index 0000000000..d2f4a51b78 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SetTarget = {} + +SetTarget.NAME = "SetTarget" +SetTarget.ID = 0x0000 +SetTarget.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "latch", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function SetTarget:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function SetTarget:init(device, endpoint_id, position, latch, speed) + local out = {} + local args = {position, latch, speed} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = SetTarget, + __tostring = SetTarget.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function SetTarget:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SetTarget:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function SetTarget:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SetTarget, {__call = SetTarget.init}) + +return SetTarget diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua new file mode 100644 index 0000000000..ce70c7e680 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Step = {} + +Step.NAME = "Step" +Step.ID = 0x0001 +Step.field_defs = { + { + name = "direction", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "embedded_clusters.ClosureDimension.types.StepDirectionEnum", + }, + { + name = "number_of_steps", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function Step:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function Step:init(device, endpoint_id, direction, number_of_steps, speed) + local out = {} + local args = {direction, number_of_steps, speed} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Step, + __tostring = Step.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function Step:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Step:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function Step:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Step, {__call = Step.init}) + +return Step diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua new file mode 100644 index 0000000000..74003acc00 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua @@ -0,0 +1,19 @@ +local command_mt = {} +command_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureDimension.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureDimensionServerCommands = {} + +function ClosureDimensionServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureDimensionServerCommands, command_mt) + +return ClosureDimensionServerCommands diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua new file mode 100644 index 0000000000..56958b2020 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local DimensionStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "DimensionStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +DimensionStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +DimensionStateStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +DimensionStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = DimensionStateStruct.init +new_mt.__index.serialize = DimensionStateStruct.serialize + +DimensionStateStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(DimensionStateStruct, new_mt) + +return DimensionStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua new file mode 100644 index 0000000000..ecf029105c --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua @@ -0,0 +1,207 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.POSITIONING = 0x0001 +Feature.MOTION_LATCHING = 0x0002 +Feature.UNIT = 0x0004 +Feature.LIMITATION = 0x0008 +Feature.SPEED = 0x0010 +Feature.TRANSLATION = 0x0020 +Feature.ROTATION = 0x0040 +Feature.MODULATION = 0x0080 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + POSITIONING = 0x0001, + MOTION_LATCHING = 0x0002, + UNIT = 0x0004, + LIMITATION = 0x0008, + SPEED = 0x0010, + TRANSLATION = 0x0020, + ROTATION = 0x0040, + MODULATION = 0x0080, +} + +Feature.is_positioning_set = function(self) + return (self.value & self.POSITIONING) ~= 0 +end + +Feature.set_positioning = function(self) + if self.value ~= nil then + self.value = self.value | self.POSITIONING + else + self.value = self.POSITIONING + end +end + +Feature.unset_positioning = function(self) + self.value = self.value & (~self.POSITIONING & self.BASE_MASK) +end + +Feature.is_motion_latching_set = function(self) + return (self.value & self.MOTION_LATCHING) ~= 0 +end + +Feature.set_motion_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.MOTION_LATCHING + else + self.value = self.MOTION_LATCHING + end +end + +Feature.unset_motion_latching = function(self) + self.value = self.value & (~self.MOTION_LATCHING & self.BASE_MASK) +end + +Feature.is_unit_set = function(self) + return (self.value & self.UNIT) ~= 0 +end + +Feature.set_unit = function(self) + if self.value ~= nil then + self.value = self.value | self.UNIT + else + self.value = self.UNIT + end +end + +Feature.unset_unit = function(self) + self.value = self.value & (~self.UNIT & self.BASE_MASK) +end + +Feature.is_limitation_set = function(self) + return (self.value & self.LIMITATION) ~= 0 +end + +Feature.set_limitation = function(self) + if self.value ~= nil then + self.value = self.value | self.LIMITATION + else + self.value = self.LIMITATION + end +end + +Feature.unset_limitation = function(self) + self.value = self.value & (~self.LIMITATION & self.BASE_MASK) +end + +Feature.is_speed_set = function(self) + return (self.value & self.SPEED) ~= 0 +end + +Feature.set_speed = function(self) + if self.value ~= nil then + self.value = self.value | self.SPEED + else + self.value = self.SPEED + end +end + +Feature.unset_speed = function(self) + self.value = self.value & (~self.SPEED & self.BASE_MASK) +end + +Feature.is_translation_set = function(self) + return (self.value & self.TRANSLATION) ~= 0 +end + +Feature.set_translation = function(self) + if self.value ~= nil then + self.value = self.value | self.TRANSLATION + else + self.value = self.TRANSLATION + end +end + +Feature.unset_translation = function(self) + self.value = self.value & (~self.TRANSLATION & self.BASE_MASK) +end + +Feature.is_rotation_set = function(self) + return (self.value & self.ROTATION) ~= 0 +end + +Feature.set_rotation = function(self) + if self.value ~= nil then + self.value = self.value | self.ROTATION + else + self.value = self.ROTATION + end +end + +Feature.unset_rotation = function(self) + self.value = self.value & (~self.ROTATION & self.BASE_MASK) +end + +Feature.is_modulation_set = function(self) + return (self.value & self.MODULATION) ~= 0 +end + +Feature.set_modulation = function(self) + if self.value ~= nil then + self.value = self.value | self.MODULATION + else + self.value = self.MODULATION + end +end + +Feature.unset_modulation = function(self) + self.value = self.value & (~self.MODULATION & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.POSITIONING | + Feature.MOTION_LATCHING | + Feature.UNIT | + Feature.LIMITATION | + Feature.SPEED | + Feature.TRANSLATION | + Feature.ROTATION | + Feature.MODULATION + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_positioning_set = Feature.is_positioning_set, + set_positioning = Feature.set_positioning, + unset_positioning = Feature.unset_positioning, + is_motion_latching_set = Feature.is_motion_latching_set, + set_motion_latching = Feature.set_motion_latching, + unset_motion_latching = Feature.unset_motion_latching, + is_unit_set = Feature.is_unit_set, + set_unit = Feature.set_unit, + unset_unit = Feature.unset_unit, + is_limitation_set = Feature.is_limitation_set, + set_limitation = Feature.set_limitation, + unset_limitation = Feature.unset_limitation, + is_speed_set = Feature.is_speed_set, + set_speed = Feature.set_speed, + unset_speed = Feature.unset_speed, + is_translation_set = Feature.is_translation_set, + set_translation = Feature.set_translation, + unset_translation = Feature.unset_translation, + is_rotation_set = Feature.is_rotation_set, + set_rotation = Feature.set_rotation, + unset_rotation = Feature.unset_rotation, + is_modulation_set = Feature.is_modulation_set, + set_modulation = Feature.set_modulation, + unset_modulation = Feature.unset_modulation, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua new file mode 100644 index 0000000000..c33e8a23ef --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua @@ -0,0 +1,27 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local StepDirectionEnum = {} +local new_mt = UintABC.new_mt({NAME = "StepDirectionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.DECREASE] = "DECREASE", + [self.INCREASE] = "INCREASE", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.DECREASE = 0x00 +new_mt.__index.INCREASE = 0x01 + +StepDirectionEnum.DECREASE = 0x00 +StepDirectionEnum.INCREASE = 0x01 + +StepDirectionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(StepDirectionEnum, new_mt) + +return StepDirectionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua new file mode 100644 index 0000000000..b84a353442 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua @@ -0,0 +1,57 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local TranslationDirectionEnum = {} +local new_mt = UintABC.new_mt({NAME = "TranslationDirectionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.DOWNWARD] = "DOWNWARD", + [self.UPWARD] = "UPWARD", + [self.VERTICAL_MASK] = "VERTICAL_MASK", + [self.VERTICAL_SYMMETRY] = "VERTICAL_SYMMETRY", + [self.LEFTWARD] = "LEFTWARD", + [self.RIGHTWARD] = "RIGHTWARD", + [self.HORIZONTAL_MASK] = "HORIZONTAL_MASK", + [self.HORIZONTAL_SYMMETRY] = "HORIZONTAL_SYMMETRY", + [self.FORWARD] = "FORWARD", + [self.BACKWARD] = "BACKWARD", + [self.DEPTH_MASK] = "DEPTH_MASK", + [self.DEPTH_SYMMETRY] = "DEPTH_SYMMETRY", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.DOWNWARD = 0x00 +new_mt.__index.UPWARD = 0x01 +new_mt.__index.VERTICAL_MASK = 0x02 +new_mt.__index.VERTICAL_SYMMETRY = 0x03 +new_mt.__index.LEFTWARD = 0x04 +new_mt.__index.RIGHTWARD = 0x05 +new_mt.__index.HORIZONTAL_MASK = 0x06 +new_mt.__index.HORIZONTAL_SYMMETRY = 0x07 +new_mt.__index.FORWARD = 0x08 +new_mt.__index.BACKWARD = 0x09 +new_mt.__index.DEPTH_MASK = 0x0A +new_mt.__index.DEPTH_SYMMETRY = 0x0B + +TranslationDirectionEnum.DOWNWARD = 0x00 +TranslationDirectionEnum.UPWARD = 0x01 +TranslationDirectionEnum.VERTICAL_MASK = 0x02 +TranslationDirectionEnum.VERTICAL_SYMMETRY = 0x03 +TranslationDirectionEnum.LEFTWARD = 0x04 +TranslationDirectionEnum.RIGHTWARD = 0x05 +TranslationDirectionEnum.HORIZONTAL_MASK = 0x06 +TranslationDirectionEnum.HORIZONTAL_SYMMETRY = 0x07 +TranslationDirectionEnum.FORWARD = 0x08 +TranslationDirectionEnum.BACKWARD = 0x09 +TranslationDirectionEnum.DEPTH_MASK = 0x0A +TranslationDirectionEnum.DEPTH_SYMMETRY = 0x0B + +TranslationDirectionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(TranslationDirectionEnum, new_mt) + +return TranslationDirectionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua new file mode 100644 index 0000000000..8bc2fe2cb2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua @@ -0,0 +1,10 @@ +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.ClosureDimension.types." .. key) +end + +local ClosureDimensionTypes = {} + +setmetatable(ClosureDimensionTypes, types_mt) + +return ClosureDimensionTypes diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 6759560c50..66581d0ae8 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --Note: Currently only support for window shades with the PositionallyAware Feature --Note: No support for setting device into calibration mode, it must be done manually @@ -373,14 +363,14 @@ local matter_driver_template = { capabilities.windowShadeTiltLevel, capabilities.windowShade, capabilities.windowShadePreset, + capabilities.doorControl, + capabilities.level, capabilities.battery, capabilities.batteryLevel, }, - sub_drivers = { - -- for devices sending a position update while device is in motion - require("matter-window-covering-position-updates-while-moving") - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-window-covering", matter_driver_template) -matter_driver:run() \ No newline at end of file +matter_driver:run() diff --git a/drivers/SmartThings/matter-window-covering/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-window-covering/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua new file mode 100644 index 0000000000..ad3e53e4b0 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("sub_drivers.closure"), + lazy_load_if_possible("sub_drivers.matter-window-covering-position-updates-while-moving"), +} +return sub_drivers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua new file mode 100644 index 0000000000..a643fa909e --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CLOSURE_CONTROL_CLUSTER_ID = 0x0104 + +return function(opts, driver, device) + if #device:get_endpoints(CLOSURE_CONTROL_CLUSTER_ID) > 0 then + return true, require("sub_drivers.closure") + end + return false +end diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..c574991c82 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua @@ -0,0 +1,110 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" + +local ClosureAttrHandlers = {} + +function ClosureAttrHandlers.main_state_attr_handler(driver, device, ib, response) + if ib.data.value == nil then return end + closure_utils.set_closure_control_state(device, ib.endpoint_id, { main = ib.data.value }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) +end + +function ClosureAttrHandlers.overall_current_state_attr_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureControl.types.OverallCurrentStateStruct:augment_type(ib.data) + for _, v in pairs(ib.data.elements or {}) do + if v.field_id == 0 then + local current = v.value + closure_utils.set_closure_control_state(device, ib.endpoint_id, { current = current }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) + break + end + end +end + +function ClosureAttrHandlers.overall_target_state_attr_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureControl.types.OverallTargetStateStruct:augment_type(ib.data) + for _, v in pairs(ib.data.elements or {}) do + if v.field_id == 0 then + local target = v.value + closure_utils.set_closure_control_state(device, ib.endpoint_id, { target = target }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) + break + end + end +end + +function ClosureAttrHandlers.closure_dimension_current_state_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureDimension.types.DimensionStateStruct:augment_type(ib.data) + local pos_field = ib.data.elements.position + if not pos_field or pos_field.value == nil then return end + local level = math.floor(pos_field.value / 100) + if device:supports_capability_by_id(capabilities.doorControl.ID) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(level)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(level)) + end +end + +function ClosureAttrHandlers.tag_list_handler(driver, device, ib, response) + if not ib.data.elements then return end + local tag_value + for _, v in ipairs(ib.data.elements) do + local tag = v.elements + if tag and tag.namespace_id and tag.namespace_id.value == 0x44 then + tag_value = tag.tag and tag.tag.value + break + end + end + + local closure_tag_map = { + [0] = fields.closure_tag_list.COVERING, + [1] = fields.closure_tag_list.WINDOW, + [2] = fields.closure_tag_list.BARRIER, + [3] = fields.closure_tag_list.CABINET, + [4] = fields.closure_tag_list.GATE, + [5] = fields.closure_tag_list.GARAGE_DOOR, + [6] = fields.closure_tag_list.DOOR, + } + + local closure_tag = fields.closure_tag_list.NA + if tag_value ~= nil and closure_tag_map[tag_value] ~= nil then + closure_tag = closure_tag_map[tag_value] + end + + device:set_field(fields.CLOSURE_TAG, closure_tag, {persist = true}) + closure_utils.match_profile(device) +end + +function ClosureAttrHandlers.power_source_attribute_list_handler(driver, device, ib, response) + for _, attr in ipairs(ib.data.elements) do + if attr.value == 0x0C then -- BatPercentRemaining + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist = true}) + closure_utils.match_profile(device) + return + elseif attr.value == 0x0E then -- BatChargeLevel + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist = true}) + closure_utils.match_profile(device) + return + end + end + -- No battery attribute found + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + closure_utils.match_profile(device) +end + +return ClosureAttrHandlers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua new file mode 100644 index 0000000000..4084d0fae1 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua @@ -0,0 +1,71 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" + +local ClosureCapabilityHandlers = {} + +-- close covering (or door/gate) +function ClosureCapabilityHandlers.handle_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local reverse = device:get_field(fields.REVERSE_POLARITY) + local req = reverse and + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) or + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) + device:send(req) +end + +-- open covering (or door/gate) +function ClosureCapabilityHandlers.handle_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local reverse = device:get_field(fields.REVERSE_POLARITY) + local req = reverse and + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) or + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) + device:send(req) +end + +-- pause / stop covering +function ClosureCapabilityHandlers.handle_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ClosureControl.server.commands.Stop(device, endpoint_id)) +end + +-- move to shade level 0-100 for covering Closure devices +function ClosureCapabilityHandlers.handle_shade_level(driver, device, cmd) + local dim_eps = closure_utils.get_closure_dimension_eps(device) + local endpoint_id = #dim_eps == 1 and dim_eps[1] or device:component_to_endpoint(cmd.component) + if endpoint_id then + device:send(clusters.ClosureDimension.server.commands.SetTarget( + device, endpoint_id, cmd.args.shadeLevel * 100 + )) + end +end + +-- move to level 0-100 for door/gate/garage-door Closure devices +function ClosureCapabilityHandlers.handle_level(driver, device, cmd) + local dim_eps = closure_utils.get_closure_dimension_eps(device) + local endpoint_id = #dim_eps == 1 and dim_eps[1] or device:component_to_endpoint(cmd.component) + device:send(clusters.ClosureDimension.server.commands.SetTarget( + device, endpoint_id, cmd.args.level * 100 + )) +end + +return ClosureCapabilityHandlers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua new file mode 100644 index 0000000000..11cd32b8bd --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua @@ -0,0 +1,37 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local fields = {} + +fields.REVERSE_POLARITY = "__reverse_polarity" +fields.PRESET_LEVEL_KEY = "__preset_level_key" +fields.DEFAULT_PRESET_LEVEL = 50 + +fields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE", +} + +fields.CLOSURE_CONTROL_STATE_CACHE = "__closure_control_state_cache" +fields.CLOSURE_BATTERY_SUPPORT = "__closure_battery_support" +fields.CLOSURE_TAG = "__closure_tag" + +fields.closure_tag_list = { + NA = "N/A", + COVERING = "COVERING", + WINDOW = "WINDOW", + BARRIER = "BARRIER", + CABINET = "CABINET", + GATE = "GATE", + GARAGE_DOOR = "GARAGE_DOOR", + DOOR = "DOOR", +} + +-- The maximum number of supported panels for a closure device. Note that this is an +-- arbitrary number and should be raised if needed by a closure device with more panels. +fields.MAX_CLOSURE_PANELS = 4 + +fields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" + +return fields diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua new file mode 100644 index 0000000000..df6dc9015c --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua @@ -0,0 +1,318 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local log = require "log" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" + +local utils = {} + +function utils.find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then -- 0 is the Matter RootNode endpoint + return v + end + end + device.log.warn(string.format( + "Did not find default endpoint, will use endpoint %d instead", + device.MATTER_DEFAULT_ENDPOINT + )) + return res +end + +function utils.get_closure_dimension_eps(device) + local eps = device:get_endpoints(clusters.ClosureDimension.ID) or {} + table.sort(eps) + local result = {} + for _, ep in ipairs(eps) do + if ep ~= 0 then + table.insert(result, ep) + if #result >= fields.MAX_CLOSURE_PANELS then break end + end + end + return result +end + +--- Single-panel devices always map to "main"; +--- multi-panel devices map to "windowShade1"..."windowShade4" or "door1"..."door4". +function utils.endpoint_to_component(device, ep_id) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 1 then + local is_door_type = device:supports_capability_by_id(capabilities.doorControl.ID) + local prefix = is_door_type and "door" or "windowShade" + for i, ep in ipairs(dim_eps) do + if ep == ep_id then + return prefix .. i + end + end + end + return "main" +end + +function utils.component_to_endpoint(device, component_name) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 1 then + local comp_num = tonumber(component_name:match("(%d+)$")) + if comp_num and dim_eps[comp_num] then + return dim_eps[comp_num] + end + end + return utils.find_default_endpoint(device, clusters.ClosureControl.ID) +end + +function utils.match_profile(device) + if not device:get_field(fields.CLOSURE_TAG) or not device:get_field(fields.CLOSURE_BATTERY_SUPPORT) then + log.warn("Closure tag or battery support not set yet, cannot match profile") + return + end + + local tag = device:get_field(fields.CLOSURE_TAG) + local profile_name + local is_door_type = true + + if tag == fields.closure_tag_list.GATE then + profile_name = "gate" + elseif tag == fields.closure_tag_list.GARAGE_DOOR then + profile_name = "garage-door" + elseif tag == fields.closure_tag_list.DOOR then + profile_name = "door" + else + -- COVERING, WINDOW, BARRIER, CABINET, NA -> generic covering profile + profile_name = "covering" + is_door_type = false + end + + local optional_caps = {} + local main_component_capabilities = {} + + local closure_battery = device:get_field(fields.CLOSURE_BATTERY_SUPPORT) + if closure_battery == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + elseif closure_battery == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + end + + -- ClosureDimension capabilities: windowShadeLevel (covering) or level (door types) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 0 then + local dim_cap = is_door_type and capabilities.level.ID or capabilities.windowShadeLevel.ID + if #dim_eps == 1 then + -- Single ClosureDimension: enable the capability on the main component. + table.insert(main_component_capabilities, dim_cap) + else + -- Multiple ClosureDimensions: one optional component+capability per panel. + local prefix = is_door_type and "door" or "windowShade" + for i = 1, math.min(#dim_eps, fields.MAX_CLOSURE_PANELS) do + table.insert(optional_caps, {prefix .. i, {dim_cap}}) + end + end + end + + if #main_component_capabilities > 0 then + table.insert(optional_caps, 1, {"main", main_component_capabilities}) + end + + device:try_update_metadata({ + profile = profile_name, + optional_component_capabilities = #optional_caps > 0 and optional_caps or nil, + }) +end + +--- Deeply compare two values. +--- Handles metatables. Can optionally ignore cycle checking and/or function differences. +--- +--- @param a any +--- @param b any +--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } +--- @param seen table|nil +--- @return boolean +function utils.deep_equals(a, b, opts, seen) + if a == b then return true end + if type(a) ~= type(b) then return false end + if type(a) == "function" and opts and opts.ignore_functions then return true end + if type(a) ~= "table" then return false end + + if not (opts and opts.ignore_cycles) then + seen = seen or {} + seen[a] = seen[a] or {} + if seen[a][b] then + return seen[a][b] + end + seen[a][b] = true + end + + for k, v in pairs(a) do + if not utils.deep_equals(v, b[k], opts, seen) then + return false + end + end + + for k in pairs(b) do + if a[k] == nil then + return false + end + end + + local mt_a = getmetatable(a) + local mt_b = getmetatable(b) + return utils.deep_equals(mt_a, mt_b, opts, seen) +end + +function utils.set_closure_control_state(device, endpoint_id, field) + local cache = device:get_field(fields.CLOSURE_CONTROL_STATE_CACHE) or {} + if not cache[endpoint_id] then cache[endpoint_id] = {} end + for k, v in pairs(field) do + cache[endpoint_id][k] = v + end + device:set_field(fields.CLOSURE_CONTROL_STATE_CACHE, cache) +end + +--- Emits the appropriate windowShade / doorControl capability event from the +--- cached MainState, OverallCurrentState.position, and OverallTargetState.position. +function utils.emit_closure_control_capability(device, endpoint_id) + local cache = device:get_field(fields.CLOSURE_CONTROL_STATE_CACHE) + if not cache then return end + local closure_control_state = cache[endpoint_id] or {} + local reverse = device:get_field(fields.REVERSE_POLARITY) + + local main = closure_control_state.main + local current = closure_control_state.current + local target = closure_control_state.target + + local closure_capability = capabilities.windowShade.windowShade + if device:supports_capability_by_id(capabilities.doorControl.ID) then + closure_capability = capabilities.doorControl.door + end + + if main == clusters.ClosureControl.types.MainStateEnum.MOVING then + if target == clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.opening() or closure_capability.closing() + ) + elseif target == clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.closing() or closure_capability.opening() + ) + end + elseif main == clusters.ClosureControl.types.MainStateEnum.STOPPED or main == nil then + if current == nil then return end + if current == clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.open() or closure_capability.closed() + ) + elseif current == clusters.ClosureControl.types.CurrentPositionEnum.FULLY_OPENED or + device:supports_capability_by_id(capabilities.doorControl.ID) then + -- doorControl does not support partially_open; treat any non-fully-closed as open + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.closed() or closure_capability.open() + ) + else + device:emit_event_for_endpoint(endpoint_id, closure_capability.partially_open()) + end + end +end + +--- helper for the switch subscribe override, which adds to a subscribed request for a checked device +--- +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request table a subscribe request that will be appended to as needed for the device +--- @param capabilities_seen table a list of capabilities that have already been checked by previously handled devices +--- @param attributes_seen table a list of attributes that have already been checked +--- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes +function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, subscribed_attributes) + for _, component in pairs(checked_device.st_store.profile.components) do + for _, capability in pairs(component.capabilities) do + if not capabilities_seen[capability.id] then + for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + capabilities_seen[capability.id] = true -- only loop through any capability once + end + end + end +end + +function utils.subscribe(device) + local closure_subscribed_attributes = { + [capabilities.windowShade.ID] = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, + }, + [capabilities.doorControl.ID] = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, + }, + [capabilities.windowShadeLevel.ID] = { + clusters.ClosureDimension.attributes.CurrentState, + }, + [capabilities.level.ID] = { + clusters.ClosureDimension.attributes.CurrentState, + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + }, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, + }, + } + + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local capabilities_seen, attributes_seen = {}, {} + local additional_attributes = {} + + -- The refresh capability command handler in the lua libs uses this key to determine which attributes to read. + device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen) + + -- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of + -- subscribed attributes in order to determine which if any battery capability should be used. + if device:get_field(fields.CLOSURE_BATTERY_SUPPORT) == nil then + local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) + end + + if device:get_field(fields.CLOSURE_TAG) == nil then + table.insert(additional_attributes, clusters.Descriptor.attributes.TagList) + end + + utils.populate_subscribe_request_for_device( + device, subscribe_request, capabilities_seen, attributes_seen, closure_subscribed_attributes + ) + + for _, attr in ipairs(additional_attributes) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end +end + +return utils diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua new file mode 100644 index 0000000000..638f561415 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua @@ -0,0 +1,140 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" +local attribute_handlers = require "sub_drivers.closure.closure_handlers.attribute_handlers" +local capability_handlers = require "sub_drivers.closure.closure_handlers.capability_handlers" + +-- --------------------------------------------------------------------------- +-- Lifecycle handlers +-- --------------------------------------------------------------------------- + +local ClosureLifecycleHandlers = {} + +function ClosureLifecycleHandlers.device_init(driver, device) + device:set_component_to_endpoint_fn(closure_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(closure_utils.endpoint_to_component) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, + capabilities.windowShadePreset.position.NAME) == nil then + device:emit_event(capabilities.windowShadePreset.supportedCommands( + {"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}} + )) + local preset_position = device:get_field(fields.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + fields.DEFAULT_PRESET_LEVEL + device:emit_event(capabilities.windowShadePreset.position( + preset_position, {visibility = {displayed = false}} + )) + device:set_field(fields.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end + device:extend_device("subscribe", closure_utils.subscribe) + device:subscribe() +end + +function ClosureLifecycleHandlers.device_added(driver, device) + if device:supports_capability_by_id(capabilities.windowShade.ID) then + device:emit_event( + capabilities.windowShade.supportedWindowShadeCommands( + {"open", "close", "pause"}, {visibility = {displayed = false}} + ) + ) + end + device:set_field(fields.REVERSE_POLARITY, false, {persist = true}) +end + +function ClosureLifecycleHandlers.do_configure(driver, device) + if #device:get_endpoints(clusters.Descriptor.ID) == 0 then + log.warn( + "Descriptor cluster not implemented on ClosureControl endpoint, " .. + "cannot read TagList to determine closure type" + ) + device:set_field(fields.CLOSURE_TAG, fields.closure_tag_list.NA, {persist = true}) + end + + local battery_feature_eps = device:get_endpoints( + clusters.PowerSource.ID, + {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + ) + if #battery_feature_eps == 0 then + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + closure_utils.match_profile(device) + end +end + +function ClosureLifecycleHandlers.info_changed(driver, device, event, args) + if not closure_utils.deep_equals( + device.profile, args.old_st_store.profile, {ignore_functions = true} + ) then + device:subscribe() + elseif args.old_st_store.preferences.reverse ~= device.preferences.reverse then + if device.preferences.reverse then + device:set_field(fields.REVERSE_POLARITY, true, {persist = true}) + else + device:set_field(fields.REVERSE_POLARITY, false, {persist = true}) + end + end +end + +-- --------------------------------------------------------------------------- +-- Subdriver template +-- --------------------------------------------------------------------------- + +local closure_handler = { + NAME = "Closure Handler", + lifecycle_handlers = { + init = ClosureLifecycleHandlers.device_init, + added = ClosureLifecycleHandlers.device_added, + doConfigure = ClosureLifecycleHandlers.do_configure, + infoChanged = ClosureLifecycleHandlers.info_changed, + }, + matter_handlers = { + attr = { + [clusters.ClosureControl.ID] = { + [clusters.ClosureControl.attributes.MainState.ID] = attribute_handlers.main_state_attr_handler, + [clusters.ClosureControl.attributes.OverallCurrentState.ID] = attribute_handlers.overall_current_state_attr_handler, + [clusters.ClosureControl.attributes.OverallTargetState.ID] = attribute_handlers.overall_target_state_attr_handler, + }, + [clusters.ClosureDimension.ID] = { + [clusters.ClosureDimension.attributes.CurrentState.ID] = attribute_handlers.closure_dimension_current_state_handler, + }, + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.TagList.ID] = attribute_handlers.tag_list_handler, + }, + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + }, + }, + }, + capability_handlers = { + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = capability_handlers.handle_close, + [capabilities.windowShade.commands.open.NAME] = capability_handlers.handle_open, + [capabilities.windowShade.commands.pause.NAME] = capability_handlers.handle_pause, + }, + [capabilities.doorControl.ID] = { + [capabilities.doorControl.commands.open.NAME] = capability_handlers.handle_open, + [capabilities.doorControl.commands.close.NAME] = capability_handlers.handle_close, + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = capability_handlers.handle_shade_level, + }, + [capabilities.level.ID] = { + [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_level, + }, + }, + can_handle = require("sub_drivers.closure.can_handle"), +} + +return closure_handler diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua new file mode 100644 index 0000000000..e3e12116e1 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) + local device_lib = require "st.device" + if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then + return false + end + local FINGERPRINTS = require("sub_drivers.matter-window-covering-position-updates-while-moving.fingerprints") + for i, v in ipairs(FINGERPRINTS) do + if device.manufacturer_info.vendor_id == v[1] and + device.manufacturer_info.product_id == v[2] then + return true, require("sub_drivers.matter-window-covering-position-updates-while-moving") + end + end + return false +end + +return is_matter_window_covering_position_updates_while_moving diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua new file mode 100644 index 0000000000..37800fc680 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SUB_WINDOW_COVERING_VID_PID = { + {0x10e1, 0x1005} -- VDA +} + +return SUB_WINDOW_COVERING_VID_PID diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua similarity index 81% rename from drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua rename to drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua index 11ad2d8ef9..56407c6667 100644 --- a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua @@ -1,20 +1,9 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local device_lib = require "st.device" local DEFAULT_LEVEL = 0 local STATE_MACHINE = "__state_machine" @@ -27,22 +16,7 @@ local StateMachineEnum = { STATE_CURRENT_POSITION_FIRED = 0x03 } -local SUB_WINDOW_COVERING_VID_PID = { - {0x10e1, 0x1005} -- VDA -} -local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - for i, v in ipairs(SUB_WINDOW_COVERING_VID_PID) do - if device.manufacturer_info.vendor_id == v[1] and - device.manufacturer_info.product_id == v[2] then - return true - end - end - return false -end local function device_init(driver, device) device:subscribe() @@ -145,7 +119,8 @@ local matter_window_covering_position_updates_while_moving_handler = { }, capability_handlers = { }, - can_handle = is_matter_window_covering_position_updates_while_moving, + can_handle = require("matter-window-covering-position-updates-while-moving.can_handle"), + shared_device_thread_enabled = true, } return matter_window_covering_position_updates_while_moving_handler diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua new file mode 100644 index 0000000000..95a4c6f112 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua @@ -0,0 +1,785 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +-- --------------------------------------------------------------------------- +-- Mock device: Covering type (windowShade / windowShadeLevel) +-- --------------------------------------------------------------------------- + +local mock_device = test.mock_device.build_test_matter_device( + { + label = "Matter Closure", + profile = t_utils.get_profile_definition("covering.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.ClosureControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + device_types = { + {device_type_id = 0x0230, device_type_revision = 1} -- Closure + } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + }, + } +) + +-- --------------------------------------------------------------------------- +-- Mock device: Door type (doorControl / level) +-- --------------------------------------------------------------------------- + +local mock_door_device = test.mock_device.build_test_matter_device( + { + label = "Matter Door", + profile = t_utils.get_profile_definition("door.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.ClosureControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + device_types = { + {device_type_id = 0x0230, device_type_revision = 1} -- Closure + } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + }, + } +) + +local CLUSTER_SUBSCRIBE_LIST = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, +} + +-- additional clusters that will be subscribed to initially but not after the profile is matched. +local ADDITIONAL_SUBSCRIBE_LIST = { + clusters.PowerSource.attributes.AttributeList, + clusters.Descriptor.attributes.TagList, +} + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + for _, clus in ipairs(ADDITIONAL_SUBSCRIBE_LIST) do + subscribe_request:merge(clus:subscribe(mock_device)) + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +local function update_profile() + test.socket.matter:__queue_receive({mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + test.socket.matter:__queue_receive({mock_device.id, clusters.Descriptor.attributes.TagList:build_test_report_data( + mock_device, 10, {clusters.Global.types.SemanticTagStruct({mfg_code = 0x00, namespace_id = 0x44, tag = 0x00, name = "Covering"}) } + )}) + mock_device:expect_metadata_update({ + profile = "covering", + optional_component_capabilities = { + {"main", {"battery"}}, + {"windowShade1", {"windowShadeLevel"}}, + {"windowShade2", {"windowShadeLevel"}}, + } + }) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition("covering.yml", { + enabled_optional_capabilities = { + {"main", {"battery"}}, + {"windowShade1", {"windowShadeLevel"}}, + {"windowShade2", {"windowShadeLevel"}}, + } + }) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + subscribe_request:merge(clusters.PowerSource.server.attributes.BatPercentRemaining:subscribe(mock_device)) + subscribe_request:merge(clusters.ClosureDimension.attributes.CurrentState:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +local function test_init_door() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_door_device) + test.socket.device_lifecycle:__queue_receive({ mock_door_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_door_device.id, "init" }) + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_door_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_door_device)) end + end + for _, clus in ipairs(ADDITIONAL_SUBSCRIBE_LIST) do + subscribe_request:merge(clus:subscribe(mock_door_device)) + end + test.socket.matter:__expect_send({mock_door_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_door_device.id, "doConfigure" }) + mock_door_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function update_profile_door() + test.socket.matter:__queue_receive({mock_door_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_door_device, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + test.socket.matter:__queue_receive({mock_door_device.id, clusters.Descriptor.attributes.TagList:build_test_report_data( + mock_door_device, 10, {clusters.Global.types.SemanticTagStruct({mfg_code = 0x00, namespace_id = 0x44, tag = 0x06, name = "Door"})} + )}) + mock_door_device:expect_metadata_update({ + profile = "door", + optional_component_capabilities = { + {"main", {"battery"}}, + {"door1", {"level"}}, + {"door2", {"level"}}, + } + }) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition("door.yml", { + enabled_optional_capabilities = { + {"main", {"battery"}}, + {"door1", {"level"}}, + {"door2", {"level"}}, + } + }) + test.socket.device_lifecycle:__queue_receive(mock_door_device:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_door_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_door_device)) end + end + subscribe_request:merge(clusters.PowerSource.server.attributes.BatPercentRemaining:subscribe(mock_door_device)) + subscribe_request:merge(clusters.ClosureDimension.attributes.CurrentState:subscribe(mock_door_device)) + test.socket.matter:__expect_send({mock_door_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "windowShade closed following MainState and OverallTargetState update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + end +) + +test.register_coroutine_test( + "windowShade opening following MainState and OverallTargetState update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + end +) + +test.register_coroutine_test( + "windowShade closed following OverallCurrentState FULLY_CLOSED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + end +) + +test.register_coroutine_test( + "windowShade open following OverallCurrentState FULLY_OPENED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_OPENED, + latch = true, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + end +) + +test.register_coroutine_test( + "windowShade partially_open following OverallCurrentState PARTIALLY_OPENED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.PARTIALLY_OPENED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + end +) + +test.register_coroutine_test( + "windowShade state transitions from closing to closed", function() + update_profile() + test.wait_for_events() + -- device starts moving toward closed + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + test.wait_for_events() + -- device stops and reports fully closed + -- MainState STOPPED with no current position cached yet. no capability event emitted + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.STOPPED), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + end +) + +test.register_coroutine_test( + "windowShade close command sends ClosureControl MoveTo FULLY_CLOSED", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "close", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) + }) + end +) + +test.register_coroutine_test( + "windowShade open command sends ClosureControl MoveTo FULLY_OPEN", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) + }) + end +) + +test.register_coroutine_test( + "windowShade pause command sends ClosureControl Stop", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "pause", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.Stop(mock_device, 10) + }) + end +) + +test.register_coroutine_test( + "Battery percentage reported correctly for closure device", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 10, 150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5))) + ) + end +) + +test.register_coroutine_test( + "setShadeLevel on windowShade1 sends SetTarget to endpoint 11", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "windowShade1", command = "setShadeLevel", args = {75}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureDimension.server.commands.SetTarget(mock_device, 11, 75 * 100) + }) + end +) + +test.register_coroutine_test( + "setShadeLevel on windowShade2 sends SetTarget to endpoint 12", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "windowShade2", command = "setShadeLevel", args = {40}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureDimension.server.commands.SetTarget(mock_device, 12, 40 * 100) + }) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 11 emits shadeLevel on windowShade1", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 6000, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(60)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 12 emits shadeLevel on windowShade2", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 12, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 2500, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade2", capabilities.windowShadeLevel.shadeLevel(25)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState with closed position emits shadeLevel 0", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 0, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(0)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState with full-open position emits shadeLevel 100", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 10000, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(100)) + ) + end +) + +-- --------------------------------------------------------------------------- +-- Door / garage-door / gate type tests +-- --------------------------------------------------------------------------- + +test.register_coroutine_test( + "doorControl closed following MainState and OverallTargetState update", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_door_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_door_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("main", capabilities.doorControl.door.closing()) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl opening following MainState and OverallTargetState update", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_door_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_door_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("main", capabilities.doorControl.door.opening()) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl closed following OverallCurrentState FULLY_CLOSED", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_door_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("main", capabilities.doorControl.door.closed()) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl open following OverallCurrentState FULLY_OPENED", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_door_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_OPENED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("main", capabilities.doorControl.door.open()) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl open following OverallCurrentState PARTIALLY_OPENED (doorControl has no partially_open)", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_door_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.PARTIALLY_OPENED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + -- doorControl has no partially_open state; any non-fully-closed position maps to open + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("main", capabilities.doorControl.door.open()) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl close command sends ClosureControl MoveTo FULLY_CLOSED", function() + test.socket.capability:__queue_receive({ + mock_door_device.id, + {capability = "doorControl", component = "main", command = "close", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_door_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_door_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) + }) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "doorControl open command sends ClosureControl MoveTo FULLY_OPEN", function() + test.socket.capability:__queue_receive({ + mock_door_device.id, + {capability = "doorControl", component = "main", command = "open", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_door_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_door_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) + }) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 11 emits level on door1 for door device", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_door_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 7500, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("door1", capabilities.level.level(75)) + ) + end, + {test_init = test_init_door} +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 12 emits level on door2 for door device", function() + update_profile_door() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_door_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_door_device, 12, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 3000, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_door_device:generate_test_message("door2", capabilities.level.level(30)) + ) + end, + {test_init = test_init_door} +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 062b199ea1..9f273037f3 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/philips-hue/profiles/legacy-color.yml b/drivers/SmartThings/philips-hue/profiles/legacy-color.yml index fa3adedb6d..2d906dab24 100644 --- a/drivers/SmartThings/philips-hue/profiles/legacy-color.yml +++ b/drivers/SmartThings/philips-hue/profiles/legacy-color.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: samsungim.hueSyncMode diff --git a/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml b/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml index b7c6efc7eb..354b8bbc2e 100644 --- a/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml +++ b/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml @@ -6,8 +6,12 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml b/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml index 35fa5550bd..7fe8797be2 100644 --- a/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml +++ b/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml @@ -6,10 +6,14 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/profiles/white.yml b/drivers/SmartThings/philips-hue/profiles/white.yml index 447ddcad81..18312c48e2 100644 --- a/drivers/SmartThings/philips-hue/profiles/white.yml +++ b/drivers/SmartThings/philips-hue/profiles/white.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index cf30834a2c..6bee932239 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -19,9 +19,7 @@ local CommandHandlers = {} ---@param driver HueDriver ---@param device HueChildDevice ----@param args table -local function do_switch_action(driver, device, args) - local on = args.command == "on" +local function get_light_device_id_and_hue_api_module(driver, device) local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) @@ -49,14 +47,19 @@ local function do_switch_action(driver, device, args) return end - local resp, err = hue_api:set_light_on_state(light_id, on) + return light_id, hue_api +end - if not resp or (resp.errors and #resp.errors == 0) then +---@param response table? Command response from the Hue API, expected to have an 'errors' field if there were issues +---@param err string? Error message returned from the Hue API call, if any +---@param action_desc string Description of the action being performed, for logging purposes +local function log_command_response_errors(response, err, action_desc) + if not response or (response.errors and #response.errors == 0) then if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) + log.error_with({ hub_logs = true }, "Error performing " .. action_desc .. ": " .. err) + elseif response and #response.errors > 0 then + for _, error in ipairs(response.errors) do + log.error_with({ hub_logs = true }, "Error returned in Hue response for " .. action_desc .. ": " .. error.description) end end end @@ -65,60 +68,30 @@ end ---@param driver HueDriver ---@param device HueChildDevice ---@param args table -local function do_switch_level_action(driver, device, args) - local level = st_utils.clamp_value(args.args.level, 1, 100) - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end +local function do_switch_action(driver, device, args) + local on = args.command == "on" + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] + local resp, err = hue_api:set_light_on_state(light_id, on) + log_command_response_errors(resp, err, "on/off action") +end - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_switch_level_action(driver, device, args) + local level = st_utils.clamp_value(args.args.level, 1, 100) + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local is_off = device:get_field(Fields.SWITCH_STATE) == "off" - if is_off then local resp, err = hue_api:set_light_on_state(light_id, true) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "on/off action") end - local resp, err = hue_api:set_light_level(light_id, level) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing switch level action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "switch level action") end ---@param driver HueDriver @@ -130,46 +103,13 @@ local function do_color_action(driver, device, args) hue = 0 device:set_field(Fields.WRAPPED_HUE, true) end - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end - - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local red, green, blue = st_utils.hsv_to_rgb(hue, sat) local xy = HueColorUtils.safe_rgb_to_xy(red, green, blue, device:get_field(Fields.GAMUT)) - local resp, err = hue_api:set_light_color_xy(light_id, xy) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing color action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "color action") end -- Function to allow changes to "setHue" attribute to Philips Hue light devices @@ -207,51 +147,58 @@ end ---@param args table local function do_color_temp_action(driver, device, args) local kelvin = args.args.temperature - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end - - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local min = device:get_field(Fields.MIN_KELVIN) or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE local clamped_kelvin = st_utils.clamp_value(kelvin, min, Consts.MAX_TEMP_KELVIN) local mirek = math.floor(utils.kelvin_to_mirek(clamped_kelvin)) local resp, err = hue_api:set_light_color_temp(light_id, mirek) - - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing color temp action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "color temp action") device:set_field(Fields.COLOR_TEMP_SETPOINT, clamped_kelvin); end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_step_level_action(driver, device, args) + local step_percent = args.args and args.args.stepSize or 0 + if step_percent == 0 then return end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end + + -- stepSize is already in percent; Hue brightness_delta is also in percent + local action = (step_percent > 0) and "up" or "down" + local brightness_delta = math.abs(step_percent) + local resp, err = hue_api:set_light_level_delta(light_id, brightness_delta, action) + log_command_response_errors(resp, err, "step level action") +end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_step_color_temp_action(driver, device, args) + local step_percent = args.args and args.args.stepSize or 0 + if step_percent == 0 then return end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end + + -- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP + local action = (step_percent > 0) and "down" or "up" + + -- Derive the mirek range from stored Kelvin bounds (note: higher Kelvin = lower mirek) + local min_kelvin = device:get_field(Fields.MIN_KELVIN) or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE + local max_kelvin = device:get_field(Fields.MAX_KELVIN) or Consts.MAX_TEMP_KELVIN + local min_mirek = math.floor(utils.kelvin_to_mirek(max_kelvin)) + local max_mirek = math.ceil(utils.kelvin_to_mirek(min_kelvin)) + local mirek_delta = st_utils.round((max_mirek - min_mirek) * (math.abs(step_percent) / 100.0)) + + local resp, err = hue_api:set_light_color_temp_delta(light_id, mirek_delta, action) + log_command_response_errors(resp, err, "step color temp action") +end + ---@param driver HueDriver ---@param device HueChildDevice ---@param args table @@ -301,6 +248,20 @@ function CommandHandlers.set_color_temp_handler(driver, device, args) do_color_temp_action(driver, device, args) end +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +function CommandHandlers.step_level_handler(driver, device, args) + do_step_level_action(driver, device, args) +end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +function CommandHandlers.step_color_temp_handler(driver, device, args) + do_step_color_temp_action(driver, device, args) +end + local refresh_handlers = require "handlers.refresh_handlers" ---@param driver HueDriver diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index e05fa3d0a0..6039b395cc 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -507,4 +507,52 @@ function PhilipsHueApi:set_light_color_temp_by_device_type(id, mirek, device_typ end end +---@param id string +---@param brightness_delta number absolute brightness percentage delta +---@param action "up"|"down" +---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error +---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. +function PhilipsHueApi:set_light_level_delta(id, brightness_delta, action) + return self:set_light_level_delta_by_device_type(id, brightness_delta, action, HueDeviceTypes.LIGHT) +end + +function PhilipsHueApi:set_grouped_light_level_delta(id, brightness_delta, action) + return self:set_light_level_delta_by_device_type(id, brightness_delta, action, GROUPED_LIGHT) +end + +function PhilipsHueApi:set_light_level_delta_by_device_type(id, brightness_delta, action, device_type) + if type(brightness_delta) == "number" then + local url = string.format("/clip/v2/resource/%s/%s", device_type, id) + local payload = json.encode { dimming_delta = { action = action, brightness_delta = brightness_delta } } + return do_put(self, url, payload) + else + return nil, + string.format("Expected number for brightness delta, received %s", st_utils.stringify_table(brightness_delta, nil, false)) + end +end + +---@param id string +---@param mirek_delta number absolute mirek delta +---@param action "up"|"down" +---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error +---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. +function PhilipsHueApi:set_light_color_temp_delta(id, mirek_delta, action) + return self:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, HueDeviceTypes.LIGHT) +end + +function PhilipsHueApi:set_grouped_light_color_temp_delta(id, mirek_delta, action) + return self:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, GROUPED_LIGHT) +end + +function PhilipsHueApi:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, device_type) + if type(mirek_delta) == "number" then + local url = string.format("/clip/v2/resource/%s/%s", device_type, id) + local payload = json.encode { color_temperature_delta = { action = action, mirek_delta = mirek_delta } } + return do_put(self, url, payload) + else + return nil, + string.format("Expected number for color temp mirek delta, received %s", st_utils.stringify_table(mirek_delta, nil, false)) + end +end + return PhilipsHueApi diff --git a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua index 3ffd02b3b6..1c32b260e9 100644 --- a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua +++ b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua @@ -59,6 +59,8 @@ local set_color_handler = utils.safe_wrap_handler(command_handlers.set_color_han local set_hue_handler = utils.safe_wrap_handler(command_handlers.set_hue_handler) local set_saturation_handler = utils.safe_wrap_handler(command_handlers.set_saturation_handler) local set_color_temp_handler = utils.safe_wrap_handler(command_handlers.set_color_temp_handler) +local step_level_handler = utils.safe_wrap_handler(command_handlers.step_level_handler) +local step_color_temp_handler = utils.safe_wrap_handler(command_handlers.step_color_temp_handler) --- @class HueDriverDatastore --- @field public bridge_netinfo table @@ -105,6 +107,12 @@ function HueDriver.new_driver_template(dbg_config) [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temp_handler, }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = step_level_handler, + }, + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = step_color_temp_handler, + }, }, -- override the default capability message handler if batched receives are supported diff --git a/drivers/SmartThings/samsung-audio/src/command.lua b/drivers/SmartThings/samsung-audio/src/command.lua index deca7c4416..d03fe07850 100644 --- a/drivers/SmartThings/samsung-audio/src/command.lua +++ b/drivers/SmartThings/samsung-audio/src/command.lua @@ -34,6 +34,19 @@ local function is_empty(t) return not t or (type(t) == "table" and #t == 0) end +local function get_uic_response(ret, command_name) + local root = ret and ret.handler_res and ret.handler_res.root + local uic = root and root.UIC + local response = uic and uic.response + + if not uic then + log.warn(string.format("Missing UIC data in %s response", tostring(command_name))) + return nil, nil + end + + return uic, response +end + local function tr(s,mappings) return string.gsub(s, "(.)", @@ -94,8 +107,9 @@ function Command.volume(ip) if ip then local url = format_url(ip, "/UIC?cmd=GetVolume") local ret = handle_http_request(ip, url) - if ret then - response_map = { volume = ret.handler_res.root.UIC.response.volume, } + local _, response = get_uic_response(ret, "GetVolume") + if response and response.volume ~= nil then + response_map = { volume = response.volume, } end end return response_map @@ -114,8 +128,9 @@ function Command.set_volume(ip, level) local encoded_str_vol = "/UIC?cmd=%3Cpwron%3Eon%3C/pwron%3E%3Cname%3ESetVolume%3C/name%3E%3Cp%20type=%22dec%22%20name=%22volume%22%20val=%22" .. level .. "%22%3E%3C/p%3E" local url = format_url(ip, encoded_str_vol) local ret = handle_http_request(ip, url) - if ret then - response_map = { volume = ret.handler_res.root.UIC.response.volume, } + local _, response = get_uic_response(ret, "SetVolume") + if response and response.volume ~= nil then + response_map = { volume = response.volume, } end end return response_map @@ -326,8 +341,9 @@ function Command.getMute(ip) if ip then local url = format_url(ip, "/UIC?cmd=GetMute") local ret = handle_http_request(ip, url) - if ret then - response_map = { muted = ret.handler_res.root.UIC.response.mute,} + local _, response = get_uic_response(ret, "GetMute") + if response and response.mute ~= nil then + response_map = { muted = response.mute,} end end return response_map @@ -342,8 +358,9 @@ function Command.getPlayStatus(ip) if ip then local url = format_url(ip, "/UIC?cmd=GetPlayStatus") local ret = handle_http_request(ip, url) - if ret then - response_map = { playstatus = ret.handler_res.root.UIC.response.playstatus,} + local _, response = get_uic_response(ret, "GetPlayStatus") + if response and response.playstatus ~= nil then + response_map = { playstatus = response.playstatus,} end end return response_map diff --git a/drivers/SmartThings/samsung-audio/src/handlers.lua b/drivers/SmartThings/samsung-audio/src/handlers.lua index 6233a86326..65a0027bf9 100644 --- a/drivers/SmartThings/samsung-audio/src/handlers.lua +++ b/drivers/SmartThings/samsung-audio/src/handlers.lua @@ -137,7 +137,7 @@ end function CapabilityHandlers.handle_audio_notification(driver, device, cmd) local ip = device:get_field("ip") local mute_status = command.getMute(ip) - if mute_status.muted ~= "off" then + if mute_status and mute_status.muted ~= nil and mute_status.muted ~= "off" then --unmute before playig notification command.unmute(ip) end diff --git a/drivers/SmartThings/samsung-audio/src/init.lua b/drivers/SmartThings/samsung-audio/src/init.lua index de1958ff25..2e51b8e0b4 100644 --- a/drivers/SmartThings/samsung-audio/src/init.lua +++ b/drivers/SmartThings/samsung-audio/src/init.lua @@ -94,14 +94,23 @@ local function emit_refresh_data_to_server(driver, device, cmd) -- get volume local vol = command.volume(device:get_field("ip")) - device:emit_event(capabilities.audioVolume.volume(tonumber(vol.volume))) + local current_volume = vol and tonumber(vol.volume) + if current_volume ~= nil then + device:emit_event(capabilities.audioVolume.volume(current_volume)) + else + log.warn("Unable to read speaker volume from refresh response") + end -- get mute status local muteStatus = command.getMute(device:get_field("ip")) - if muteStatus.muted ~= "off" then - device:emit_event(capabilities.audioMute.mute.muted()) + if muteStatus and muteStatus.muted ~= nil then + if muteStatus.muted ~= "off" then + device:emit_event(capabilities.audioMute.mute.muted()) + else + device:emit_event(capabilities.audioMute.mute.unmuted()) + end else - device:emit_event(capabilities.audioMute.mute.unmuted()) + log.warn("Unable to read speaker mute state from refresh response") end end diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua index 993ba2a96d..49e2f79a88 100755 --- a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua @@ -33,7 +33,8 @@ local zigbee_air_quality_detector_template = { capabilities.tvocMeasurement, capabilities.tvocHealthConcern }, - sub_drivers = { require("MultiIR") } + sub_drivers = { require("MultiIR") }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_air_quality_detector_template, zigbee_air_quality_detector_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-bed/src/init.lua b/drivers/SmartThings/zigbee-bed/src/init.lua index 9f464c38ce..9d3e798b8b 100755 --- a/drivers/SmartThings/zigbee-bed/src/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/init.lua @@ -12,6 +12,7 @@ local zigbee_bed_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_bed_template, zigbee_bed_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index 8ed0db27db..cb1565ea74 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -136,6 +136,7 @@ local zigbee_button_driver_template = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_button_driver_template, zigbee_button_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua index 4ddb66aa4a..8a4f5b5c01 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua @@ -21,6 +21,7 @@ local zigbee_carbon_monoxide_driver_template = { ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_carbon_monoxide_driver_template, diff --git a/drivers/SmartThings/zigbee-contact/src/init.lua b/drivers/SmartThings/zigbee-contact/src/init.lua index 63a7bf7565..0258d5360a 100644 --- a/drivers/SmartThings/zigbee-contact/src/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/init.lua @@ -90,6 +90,7 @@ local zigbee_contact_driver_template = { sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_contact_driver_template, diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua index be7f8ad147..ed6969cbc8 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua @@ -32,6 +32,7 @@ local zigbee_dimmer_remote_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_dimmer_remote_driver_template, zigbee_dimmer_remote_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index c4a7185838..33af1cb320 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -27,6 +27,7 @@ local zigbee_fan_driver = { init = device_init }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_fan_driver,zigbee_fan_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index 9ca7cd734d..6bde23ed25 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -70,6 +70,7 @@ local zigbee_humidity_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 7f84eb68dc..6f9d3c5554 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -21,6 +21,7 @@ local zigbee_illuminance_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_illuminance_driver, zigbee_illuminance_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-lock/src/configurations.lua b/drivers/SmartThings/zigbee-lock/src/configurations.lua index a2429252b0..88e4e59f80 100644 --- a/drivers/SmartThings/zigbee-lock/src/configurations.lua +++ b/drivers/SmartThings/zigbee-lock/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..1ac2598e2f 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" @@ -445,18 +435,14 @@ local zigbee_lock_driver = { [capabilities.refresh.commands.refresh.NAME] = refresh } }, - sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { doConfigure = do_configure, added = device_added, init = init, }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_lock_driver, zigbee_lock_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua new file mode 100644 index 0000000000..543e43a8b1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_lock_without_codes(opts, driver, device) + local FINGERPRINTS = require("lock-without-codes.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("lock-without-codes") + end + end + return false +end + +return can_handle_lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua new file mode 100644 index 0000000000..63ae82b46c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return LOCK_WITHOUT_CODES_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index 7272991459..e5c6de3408 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local configurationMap = require "configurations" local clusters = require "st.zigbee.zcl.clusters" @@ -19,19 +9,7 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -95,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("lock-without-codes.can_handle"), } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..a02a59963c 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local utils = require "st.utils" local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua new file mode 100644 index 0000000000..c483b2fe27 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samsungsds_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SAMSUNG SDS" then + return true, require("samsungsds") + end + return false +end + +return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index b529dd3fd1..fff290df5d 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -112,9 +102,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("samsungsds.can_handle"), } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..ff4bf8980d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("samsungsds"), + lazy_load_if_possible("yale"), + lazy_load_if_possible("yale-fingerprint-lock"), + lazy_load_if_possible("lock-without-codes"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index 1ebee086d9..4bde45ec1a 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua index 575c4f2889..4580101cd0 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -48,4 +37,4 @@ test.register_coroutine_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua index c796789630..fcae7eda03 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 5e7590c8fa..12f8331253 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua index dc401394e9..d4ee3f770f 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua index 31ff5698c2..0b083f8050 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua index 6ca819bfeb..4e9c92bafe 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 7dfbfae4c1..95d496e786 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..a80632bf80 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local yale_fingerprint_lock_models = function(opts, driver, device) + local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yale-fingerprint-lock") + end + end + return false +end + +return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua new file mode 100644 index 0000000000..b3db27d719 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return YALE_FINGERPRINT_LOCK diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua index 9d0a0b4148..b78d043784 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -19,21 +9,7 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) @@ -48,7 +24,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("yale-fingerprint-lock.can_handle"), } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua new file mode 100644 index 0000000000..54340c7811 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function yale_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then + return true, require("yale") + end + return false +end + +return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 73e984036e..c315fbfa06 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" @@ -151,11 +142,8 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - - sub_drivers = { require("yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + sub_drivers = require("yale.sub_drivers"), + can_handle = require("yale.can_handle"), } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua new file mode 100644 index 0000000000..4b546979d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("yale.yale-bad-battery-reporter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua new file mode 100644 index 0000000000..67169e9268 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_bad_yale_lock_models = function(opts, driver, device) + local FINGERPRINTS = require("yale.yale-bad-battery-reporter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yale.yale-bad-battery-reporter") + end + end + return false +end + +return is_bad_yale_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua new file mode 100644 index 0000000000..cbb7c3404f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return BAD_YALE_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..3b77f32563 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua @@ -1,37 +1,11 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) @@ -46,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("yale.yale-bad-battery-reporter.can_handle"), } return bad_yale_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua index 660948720b..9c3f33ad14 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua @@ -125,6 +125,7 @@ local zigbee_motion_driver = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_motion_driver, zigbee_motion_driver.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index 6aa3d4b8c1..4f69f0e72e 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -60,6 +60,7 @@ local zigbee_power_meter_driver_template = { doConfigure = do_configure, }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_power_meter_driver_template, zigbee_power_meter_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index 261bc90922..d797a09b62 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -196,6 +196,7 @@ local zigbee_presence_driver = { zigbee_message_handler = all_zigbee_message_handler, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_presence_driver, zigbee_presence_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index 565aa74a28..f6ba3c1ffc 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -23,6 +23,7 @@ local zigbee_range_driver_template = { }, health_check = false, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-sensor/src/init.lua b/drivers/SmartThings/zigbee-sensor/src/init.lua index 487c19a733..348d186613 100644 --- a/drivers/SmartThings/zigbee-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-sensor/src/init.lua @@ -103,8 +103,9 @@ local zigbee_generic_sensor_template = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_generic_sensor_template, zigbee_generic_sensor_template.supported_capabilities) local zigbee_sensor = ZigbeeDriver("zigbee-sensor", zigbee_generic_sensor_template) -zigbee_sensor:run() \ No newline at end of file +zigbee_sensor:run() diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index b1ad81c9c6..8e4812c3bd 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -197,8 +197,9 @@ local zigbee_siren_driver_template = { } }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_siren_driver_template, zigbee_siren_driver_template.supported_capabilities) local zigbee_siren = ZigbeeDriver("zigbee-siren", zigbee_siren_driver_template) -zigbee_siren:run() \ No newline at end of file +zigbee_siren:run() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index fe64260c11..0f1ba0720b 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -18,6 +18,7 @@ local zigbee_smoke_driver_template = { sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_smoke_driver_template, diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 3219c46d65..b44a13d3c1 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -1735,6 +1735,11 @@ zigbeeManufacturer: manufacturer: LEDVANCE model: PLUG COMPACT EU EM T deviceProfileName: switch-power-energy + - id: "LEDVANCE/PLUG EU EM T" + deviceLabel: SMART ZIGBEE PLUG EU EM T + manufacturer: LEDVANCE + model: PLUG EU EM T + deviceProfileName: switch-power-energy - id: "OSRAM/LIGHTIFY Edge-lit flushmount" deviceLabel: SYLVANIA Light manufacturer: OSRAM @@ -2446,6 +2451,12 @@ zigbeeManufacturer: manufacturer: JNL model: Y-K002-001 deviceProfileName: basic-switch + #FIRSTLED + - id: "FIRSTLED/M4S4BAC" + deviceLabel: Mirror Series 4x4 1 + manufacturer: FIRSTLED + model: M4S4BAC + deviceProfileName: switch-light-restore-wireless zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch diff --git a/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml index 8a0d62b1f6..28d233316c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml @@ -6,8 +6,12 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml index 65ee11beb0..c3d8f2f16d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [2700, 6500] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml index 6c2da08393..07cc4b4904 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml @@ -10,8 +10,12 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml index fbe4243f6a..8395432142 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml index fae09d20cb..43b1953caf 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2000, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml index 0e542eff43..4a81aac14c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 4000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml index e8495a5b6c..420f43c959 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml index 985ec05a4f..221be95e5d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml index e6ffe1a46f..7715942435 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2500, 6000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml index 15677d1307..a1b8fa2b7c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml index b56cb5f84e..2cbe79b906 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml index c30ba1d25f..e882f23098 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml index e0c09885c4..0381032825 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml index ff670c0097..6a41cae14b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: temperatureMeasurement version: 1 - id: relativeHumidityMeasurement diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml index f39f8324eb..47368ca7e3 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 746890a15a..fe3b3de11b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: motionSensor version: 1 - id: illuminanceMeasurement diff --git a/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml b/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml index b45fc5e0e8..ac516c23c0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml index 6eca96ab18..a07047269b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml index 0d8688cb6b..dc5e6e0ca2 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml index 248bd66e7f..f344f32a34 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: motionSensor version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml index a25ef4aa2c..f5ef30908f 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml index 350c51c722..67f27f7289 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml index d6ac987d50..a234f15409 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml index c95d6c4b16..6f6a78a4b5 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1800, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml index 89af9a7f94..bf7f81832d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2000, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml index 2e17ba527d..b1c7d3e379 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 4000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml index d83b671f12..9032ba0fe0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml index 0d334ca62e..1a13390cf5 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml index 3bd54e3a59..c74ba232c0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2500, 6000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml index 740a002b83..466a34c06a 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml index 8878a04a99..dcab0e8224 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml index b863f9e587..57f566bdfb 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-button-light-restore-wireless.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-button-light-restore-wireless.yml new file mode 100644 index 0000000000..f98b552b87 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-button-light-restore-wireless.yml @@ -0,0 +1,39 @@ +name: switch-button-light-restore-wireless +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController +preferences: + - title: "背光灯(backlight/백라이트)" + name: backlight + description: "背光灯(backlight/백라이트)" + required: false + preferenceType: enumeration + definition: + options: + 0: "关闭(off/닫다)" + 1: "打开(on/열다)" + 2: "人体接近感应(proximity detection/근접 감지)" + default: 2 + - title: "开关上电状态(relay powerOn state)" + name: powerOnStatus + description: "开关上电状态(relay powerOn state/릴레이 초기 동작 상태)" + required: false + preferenceType: enumeration + definition: + options: + 0: "关闭(off/닫다)" + 1: "打开(on/열다)" + 2: "恢复记忆状态(restore/복원)" + default: 2 + - preferenceId: stse.changeToWirelessSwitch + explicit: true diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-button-wireless.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-button-wireless.yml new file mode 100644 index 0000000000..c48290e1a8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-button-wireless.yml @@ -0,0 +1,17 @@ +name: switch-button-wireless +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController +preferences: + - preferenceId: stse.changeToWirelessSwitch + explicit: true diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml index 4507ab5282..623156bf6c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml index 2042471bf3..43b2b26581 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml index 96166ef0d9..abcaba3e21 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-light-restore-wireless.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-light-restore-wireless.yml new file mode 100644 index 0000000000..b2e002d690 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-light-restore-wireless.yml @@ -0,0 +1,37 @@ +name: switch-light-restore-wireless +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +preferences: + - title: "背光灯(backlight/백라이트)" + name: backlight + description: "背光灯(backlight/백라이트)" + required: false + preferenceType: enumeration + definition: + options: + 0: "关闭(off/닫다)" + 1: "打开(on/열다)" + 2: "人体接近感应(proximity detection/근접 감지)" + default: 2 + - title: "开关上电状态(relay powerOn state)" + name: powerOnStatus + description: "开关上电状态(relay powerOn state/릴레이 초기 동작 상태)" + required: false + preferenceType: enumeration + definition: + options: + 0: "关闭(off/닫다)" + 1: "打开(on/열다)" + 2: "恢复记忆状态(restore/복원)" + default: 2 + - preferenceId: stse.changeToWirelessSwitch + explicit: true diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-wireless.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-wireless.yml new file mode 100644 index 0000000000..cb4be470f9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-wireless.yml @@ -0,0 +1,15 @@ +name: switch-wireless +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +preferences: + - preferenceId: stse.changeToWirelessSwitch + explicit: true diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua index 9280a30d0d..4b2146f03c 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -86,7 +86,7 @@ end local aqara_multi_switch_handler = { NAME = "Aqara Multi Switch Handler", lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), added = device_added }, can_handle = require("aqara.multi-switch.can_handle"), diff --git a/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/can_handle.lua new file mode 100644 index 0000000000..56cd729659 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +return function(opts, driver, device) + if device:supports_capability(capabilities.colorTemperature) then + local subdriver = require("color_temp_range_handlers") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua new file mode 100644 index 0000000000..84185557af --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua @@ -0,0 +1,75 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local switch_utils = require "switch_utils" + +-- These values are a "sanity check" to ensure that max/min values we are getting are reasonable +local COLOR_TEMPERATURE_MIRED_MAX = 1000 -- 1000 Kelvin +local COLOR_TEMPERATURE_MIRED_MIN = 67 -- 15000 Kelvin + +local function color_temp_min_mireds_handler(driver, device, value, zb_rx) + -- if mired value is nil or outside of sane bounds, log and ignore. Else, save value + local min_mired_bound = value.value + if min_mired_bound == nil then + return + elseif (min_mired_bound < COLOR_TEMPERATURE_MIRED_MIN or min_mired_bound > COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", min_mired_bound, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) + return + end + device:set_field(switch_utils.MIRED_MIN_BOUND, min_mired_bound, {persist = true}) + + -- if we have already received a valid max mired bound, emit a colorTemperatureRange event + local max_mired_bound = device:get_field(switch_utils.MIRED_MAX_BOUND) + if max_mired_bound == nil then + return + elseif min_mired_bound < max_mired_bound then + local endpoint = zb_rx.address_header.src_endpoint.value + local max_kelvin_bound = switch_utils.convert_mired_to_kelvin(min_mired_bound) + local min_kelvin_bound = switch_utils.convert_mired_to_kelvin(max_mired_bound) + device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min_kelvin_bound, maximum = max_kelvin_bound}})) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a max color temperature %d Mireds that is not higher than the reported min color temperature %d Mireds", max_mired_bound, min_mired_bound)) + end +end + +local function color_temp_max_mireds_handler(driver, device, value, zb_rx) + -- if mired value is nil or outside of sane bounds, log and ignore. Else, save value + local max_mired_bound = value.value + if max_mired_bound == nil then + return + elseif (max_mired_bound < COLOR_TEMPERATURE_MIRED_MIN or max_mired_bound > COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", max_mired_bound, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) + return + end + device:set_field(switch_utils.MIRED_MAX_BOUND, max_mired_bound, {persist = true}) + + -- if we have already received a valid min mired bound, emit a colorTemperatureRange event + local min_mired_bound = device:get_field(switch_utils.MIRED_MIN_BOUND) + if min_mired_bound == nil then + return + elseif max_mired_bound > min_mired_bound then + local endpoint = zb_rx.address_header.src_endpoint.value + local max_kelvin_bound = switch_utils.convert_mired_to_kelvin(min_mired_bound) + local min_kelvin_bound = switch_utils.convert_mired_to_kelvin(max_mired_bound) + device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min_kelvin_bound, maximum = max_kelvin_bound}})) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d Mireds that is not lower than the reported max color temperature %d Mireds", min_mired_bound, max_mired_bound)) + end +end + +local color_temp_range_handlers = { + NAME = "Color temp range handlers", + zigbee_handlers = { + attr = { + [clusters.ColorControl.ID] = { + [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = color_temp_min_mireds_handler, + [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = color_temp_max_mireds_handler + } + } + }, + can_handle = require("color_temp_range_handlers.can_handle") +} + +return color_temp_range_handlers diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua index 01f42abf91..a5e55b3953 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua @@ -84,7 +84,7 @@ configurations.handle_reporting_config_response = function(driver, device, zb_me end end -configurations.power_reconfig_wrapper = function(orig_function) +configurations.reconfig_wrapper = function(orig_function) local new_init = function(driver, device) local config_version = device:get_field(CONFIGURATION_VERSION_KEY) if config_version == nil or config_version < driver.current_config_version then @@ -92,6 +92,18 @@ configurations.power_reconfig_wrapper = function(orig_function) driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") end end + + local capabilities = require "st.capabilities" + for id, _ in pairs(device.profile.components) do + if device:supports_capability(capabilities.colorTemperature, id) and + device:get_latest_state(id, capabilities.colorTemperature.ID, capabilities.colorTemperature.colorTemperatureRange.NAME) == nil then + local clusters = require "st.zigbee.zcl.clusters" + driver:call_with_delay(5*60, function() + device:send_to_component(id, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:read(device)) + device:send_to_component(id, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:read(device)) + end) + end + end orig_function(driver, device) end return new_init diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua index 6c2d9e45e3..ff1b2deb7b 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua @@ -12,7 +12,7 @@ end local ezex_switch_handler = { NAME = "ezex switch handler", lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(do_init) + init = configurations.reconfig_wrapper(do_init) }, can_handle = require("ezex.can_handle"), } diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/can_handle.lua new file mode 100644 index 0000000000..5ad2d32a4b --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local FINGERPRINTS = require("firstled-io.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("firstled-io") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua new file mode 100644 index 0000000000..2586782642 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--The number of children determines the number of sub-devices to be created. Each sub-device has the capability to switch between a switch and a button. +--The number of buttons determines how many buttons devices will be created. +--The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. +return { + { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/init.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/init.lua new file mode 100644 index 0000000000..6348f254b2 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/init.lua @@ -0,0 +1,209 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" +local capabilities = require "st.capabilities" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local Scenes = zcl_clusters.Scenes +local PRIVATE_CLUSTER_ID = 0xFCCA +local MFG_CODE = 0x1235 +local FINGERPRINTS = require("firstled-io.fingerprints") + +--stse.changeToWirelessSwitch +--switch mode:The local switch and the app can control the relay.The button capability is not working. +--wirelessSwitch mode:The local switch does not control the relay. Once triggered, it will report to the system as "RecallScene",emit_event button.pushed. The relay can be controlled via the app. +local preference_map = { + ["backlight"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0000, + mfg_code = MFG_CODE, + data_type = data_types.Uint8, + }, + ["powerOnStatus"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0001, + mfg_code = MFG_CODE, + data_type = data_types.Uint8, + }, + ["stse.changeToWirelessSwitch"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0002, + mfg_code = MFG_CODE, + data_type = data_types.Boolean + } +} + +local function is_parent_device(device) + local parent = device:get_parent_device() + return parent == nil +end + +--If it is in the switch mode, only the switch will be displayed. If it is in the wirelessSwitch mode, both the switch and the button will be displayed. +local function toggle_button_visibility(device, show) + local is_parent = is_parent_device(device) + if is_parent then + if show then + device:try_update_metadata({profile = "switch-button-light-restore-wireless"}) + else + device:try_update_metadata({profile = "switch-light-restore-wireless"}) + end + else + if show then + device:try_update_metadata({profile = "switch-button-wireless"}) + else + device:try_update_metadata({profile = "switch-wireless"}) + end + end +end + +--When stse.changeToWirelessSwitch switching to the wirelessSwitch mode to listen for profile changes +local function listen_profile_button_transition(device, args) + local current_has = device:supports_capability(capabilities.button, "main") + + local old_has = false + if args and args.old_st_store and args.old_st_store.profile then + local old_main = args.old_st_store.profile.components.main + if old_main and old_main.capabilities then + old_has = old_main.capabilities["button"] ~= nil + end + end + --Capabilities button from non-existent to existing + if not old_has and current_has then + device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = {displayed = false}})) + device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}})) + end +end + +local function device_info_changed(driver, device, event, args) + listen_profile_button_transition(device, args) + + local preferences = device.preferences + local old_preferences = args.old_st_store.preferences + if preferences ~= nil then + for id, attr in pairs(preference_map) do + local old_value = old_preferences[id] + local value = preferences[id] + if value ~= nil and value ~= old_value then + if attr.data_type == data_types.Uint8 then + value = tonumber(value) + end + device:send(cluster_base.write_manufacturer_specific_attribute(device, attr.cluster_id, attr.attribute_id, + attr.mfg_code, attr.data_type, value)) + --Switch to the corresponding profile based on stse.changeToWirelessSwitch + if id == "stse.changeToWirelessSwitch" then + toggle_button_visibility(device, value) + end + end + end + end +end + +local function get_children_amount(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.children + end + end +end + +local function get_button_amount(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.buttons + end + end +end + +local function get_child_profile_name(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.child_profile + end + end +end + +local function find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) +end + +--Create composite switches such as 4+4, 1+1, 4+0, 3+0 children+button +local function device_added(driver, device) + -- Create the corresponding number of child devices based on the value of fingerprint.children + if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + local children_amount = get_children_amount(device) + if children_amount >= 2 then + for i = 2, children_amount, 1 do + if find_child(device, i) == nil then + local name = string.format("%s%d", string.sub(device.label, 0, -2), i) + local child_profile = get_child_profile_name(device) + local metadata = { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%02X", i), + vendor_provided_label = name + } + driver:try_create_device(metadata) + end + end + end + -- Create the corresponding number of button devices based on the value of fingerprint.buttons + local button_amount = get_button_amount(device) + if button_amount >= 1 then + for i = children_amount + 1, children_amount + button_amount, 1 do + if find_child(device, i) == nil then + local name = string.format("%s%d", string.sub(device.label, 0, -2), i) + local metadata = { + type = "EDGE_CHILD", + label = name, + profile = "button", + parent_device_id = device.id, + parent_assigned_child_key = string.format("%02X", i), + vendor_provided_label = name, + } + driver:try_create_device(metadata) + end + end + end + elseif device.network_type == "DEVICE_EDGE_CHILD" then + device:emit_event(capabilities.button.numberOfButtons({ value = 1 }, + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } })) + end +end + +local function scenes_cluster_handler(driver, device, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, + capabilities.button.button.pushed({ state_change = true })) +end + +local function device_init(self, device) + -- for multiple switch + if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + device:set_find_child(find_child) + end +end + +local firstled_switch_handler = { + NAME = "FIRSTLED Switch Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + infoChanged = device_info_changed + }, + zigbee_handlers = { + cluster = { + [Scenes.ID] = { + [Scenes.server.commands.RecallScene.ID] = scenes_cluster_handler, + } + } + }, + can_handle = require("firstled-io.can_handle"), +} + +return firstled_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua index 293583f442..d7601ab535 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua @@ -478,7 +478,7 @@ local frient_bridge_handler = { }, lifecycle_handlers = { added = added_handler, - init = init_handler, + init = configurationMap.reconfig_wrapper(init_handler), doConfigure = configure_handler, infoChanged = info_changed_handler }, diff --git a/drivers/SmartThings/zigbee-switch/src/frient/init.lua b/drivers/SmartThings/zigbee-switch/src/frient/init.lua index a615c721f4..e546de1a14 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/init.lua @@ -152,7 +152,7 @@ local frient_smart_plug = { }, }, lifecycle_handlers = { - init = device_init, + init = configurationMap.reconfig_wrapper(device_init), doConfigure = do_configure, added = device_added, }, diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua index 083893448b..0ab333af8e 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua @@ -50,7 +50,7 @@ local HanssemSwitch = { NAME = "Zigbee Hanssem Switch", lifecycle_handlers = { added = device_added, - init = configurations.power_reconfig_wrapper(device_init) + init = configurations.reconfig_wrapper(device_init) }, can_handle = require("hanssem.can_handle"), } diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua index a026ac6564..a236f1c1cc 100644 --- a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua @@ -147,7 +147,7 @@ end local ikea_xy_color_bulb = { NAME = "IKEA XY Color Bulb", lifecycle_handlers = { - init = configurationMap.power_reconfig_wrapper(device_init) + init = configurationMap.reconfig_wrapper(device_init) }, capability_handlers = { [capabilities.colorControl.ID] = { diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index aa245f5910..9fbb09119e 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -80,12 +80,13 @@ local zigbee_switch_driver_template = { }, current_config_version = 1, lifecycle_handlers = { - init = configurationMap.power_reconfig_wrapper(device_init), + init = configurationMap.reconfig_wrapper(device_init), added = lazy_handler("lifecycle_handlers.device_added"), infoChanged = lazy_handler("lifecycle_handlers.info_changed"), doConfigure = lazy_handler("lifecycle_handlers.do_configure"), }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_switch_driver_template, zigbee_switch_driver_template.supported_capabilities, diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua index 0d6fe8304b..8fad192232 100755 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua @@ -46,7 +46,7 @@ local laisiao_bath_heater = { capabilities.switch, }, lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), }, capability_handlers = { [capabilities.switch.ID] = { diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua index 465969a755..4058a953bb 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua @@ -17,4 +17,11 @@ return function(self, device) device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) end + + if device:supports_capability(capabilities.colorTemperature) then + local clusters = require "st.zigbee.zcl.clusters" + -- min and max for color temperature + device:send(clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:read(device)) + device:send(clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:read(device)) + end end diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua index ba0ed07ae3..bac6368ad2 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua @@ -49,7 +49,7 @@ end local multi_switch_no_master = { NAME = "multi switch no master", lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), added = device_added }, can_handle = require("multi-switch-no-master.can_handle"), diff --git a/drivers/SmartThings/zigbee-switch/src/robb/init.lua b/drivers/SmartThings/zigbee-switch/src/robb/init.lua index 1a8c2948c3..aa2487c87a 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/init.lua @@ -32,7 +32,7 @@ local robb_dimmer_handler = { } }, lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(do_init) + init = configurations.reconfig_wrapper(do_init) }, can_handle = require("robb.can_handle"), } diff --git a/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua new file mode 100644 index 0000000000..845bd33156 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +return function(opts, driver, device) + local can_handle = device:supports_capability(capabilities.statelessColorTemperatureStep) + or device:supports_capability(capabilities.statelessSwitchLevelStep) + if can_handle then + local subdriver = require("stateless_handlers") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua new file mode 100644 index 0000000000..07adfe0e84 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua @@ -0,0 +1,97 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local constants = require "st.zigbee.constants" +local clusters = require "st.zigbee.zcl.clusters" +local switch_utils = require "switch_utils" + +-- These values are the mired versions of the config bounds in the default profile (e.g. color-temp-bulb) +local DEFAULT_MIRED_MAX_BOUND = 370 -- 2700 Kelvin (Mireds are the inverse of Kelvin) +local DEFAULT_MIRED_MIN_BOUND = 154 -- 6500 Kelvin (Mireds are the inverse of Kelvin) + +-- Transition Time: The time that shall be taken to perform the step change, in units of 1/10ths of a second. +-- Specific fields can store custom transition times for stateless capabilities +local SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time" +local COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time" +local DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds + +-- Options Mask & Override: Indicates which options are being overridden by the Level/ColorControl cluster commands +local OPTIONS_MASK = 0x01 -- default: The `ExecuteIfOff` option is overriden +local IGNORE_COMMAND_IF_OFF = 0x00 -- default: the command will not be executed if the device is off + +-- Indicates whether a delayed refresh for ZLL devices is in progress, to prevent multiple refreshes in a quick series of step commands +local IS_REFRESH_CALLBACK_QUEUED = "__is_refresh_callback_queued" +-- Stores a timer object, which is required to cancel a timer early +local REFRESH_CALLBACK_TIMER = "__refresh_callback_timer" + +-- Note: These commands' native handlers do not match the driver's ZLL behavior 1-1. +-- Instead, they will queue a 2s timer and read refresh for each command, in all cases. +local function trigger_delayed_refresh_if_zll(device) + if device:get_profile_id() ~= constants.ZLL_PROFILE_ID then + return + end + + -- If a refresh callback is already queued, cancel it and create a new one with the updated time + if device:get_field(IS_REFRESH_CALLBACK_QUEUED) then + device.thread:cancel_timer(device:get_field(REFRESH_CALLBACK_TIMER)) + end + local delay_s = 2 + local new_timer = device.thread:call_with_delay(delay_s, function() + device:refresh() + device:set_field(IS_REFRESH_CALLBACK_QUEUED, nil) + end) + device:set_field(REFRESH_CALLBACK_TIMER, new_timer) + device:set_field(IS_REFRESH_CALLBACK_QUEUED, true) +end + +local function step_color_temperature_by_percent_handler(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local step_percent_change = cmd.args and cmd.args.stepSize or 0 + if step_percent_change == 0 then return end + local transition_time = device:get_field(COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME + -- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP + local step_mode = (step_percent_change > 0) and clusters.ColorControl.types.CcStepMode.DOWN or clusters.ColorControl.types.CcStepMode.UP + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_mireds = field_device:get_field(switch_utils.MIRED_MIN_BOUND) + local max_mireds = field_device:get_field(switch_utils.MIRED_MAX_BOUND) + -- since colorTemperatureRange is only set after both custom bounds are, use defaults if any custom bound is missing + if not (min_mireds and max_mireds) then + min_mireds = DEFAULT_MIRED_MIN_BOUND + max_mireds = DEFAULT_MIRED_MAX_BOUND + end + local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0)) + device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)) + trigger_delayed_refresh_if_zll(device) +end + +local function step_level_handler(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254) + if step_size == 0 then return end + local transition_time = device:get_field(SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME + local step_mode = (step_size > 0) and clusters.Level.types.MoveStepMode.UP or clusters.Level.types.MoveStepMode.DOWN + device:send(clusters.Level.server.commands.Step(device, step_mode, math.abs(step_size), transition_time, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)) + trigger_delayed_refresh_if_zll(device) +end + +local stateless_handlers = { + NAME = "Zigbee Stateless Step Handlers", + capability_handlers = { + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = step_color_temperature_by_percent_handler, + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = step_level_handler, + }, + }, + can_handle = require("stateless_handlers.can_handle") +} + +return stateless_handlers diff --git a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua index 69bcc2336c..ba8f097430 100644 --- a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua @@ -35,5 +35,8 @@ return { lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi"), lazy_load_if_possible("frient"), - lazy_load_if_possible("frient-IO") + lazy_load_if_possible("frient-IO"), + lazy_load_if_possible("color_temp_range_handlers"), + lazy_load_if_possible("stateless_handlers"), + lazy_load_if_possible("firstled-io") } diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua index 66ad4715f9..d30ada0588 100644 --- a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -1,8 +1,19 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" + local switch_utils = {} +switch_utils.MIRED_MAX_BOUND = "__max_mired_bound" +switch_utils.MIRED_MIN_BOUND = "__min_mired_bound" + +switch_utils.MIREDS_CONVERSION_CONSTANT = 1000000 + +switch_utils.convert_mired_to_kelvin = function(mired) + return st_utils.round(switch_utils.MIREDS_CONVERSION_CONSTANT / mired) +end + switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) if device:get_latest_state(component, capability.ID, attribute_name) == nil then device:emit_event(value) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua index db95ef787a..8826ca535d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua @@ -25,8 +25,10 @@ local zigbee_bulb_all_caps = { capabilities = { [capabilities.switch.ID] = { id = capabilities.switch.ID }, [capabilities.switchLevel.ID] = { id = capabilities.switchLevel.ID }, + [capabilities.statelessSwitchLevelStep.ID] = { id = capabilities.statelessSwitchLevelStep.ID }, [capabilities.colorControl.ID] = { id = capabilities.colorControl.ID }, [capabilities.colorTemperature.ID] = { id = capabilities.colorTemperature.ID }, + [capabilities.statelessColorTemperatureStep.ID] = { id = capabilities.statelessColorTemperatureStep.ID }, [capabilities.powerMeter.ID] = { id = capabilities.powerMeter.ID }, [capabilities.energyMeter.ID] = { id = capabilities.energyMeter.ID }, [capabilities.refresh.ID] = { id = capabilities.refresh.ID }, @@ -294,6 +296,174 @@ test.register_message_test( } ) +local DEFAULT_MIRED_MAX = 370 +local DEFAULT_MIRED_MIN = 154 +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 + +test.register_message_test( + "Step ColorTemperature command test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 43, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 194, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.UP, 108, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Step Level command test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.DOWN, 127, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 254, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + } + }, + { + min_api_version = 19 + } +) + test.register_coroutine_test( "lifecycle configure event should configure device", function () @@ -417,14 +587,161 @@ test.register_coroutine_test( mock_device.id, SimpleMetering.attributes.Divisor:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Level.attributes.CurrentLevel:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.CurrentHue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.CurrentSaturation:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + ColorControl.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 0x0010) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.CurrentHue:configure_reporting(mock_device, 1, 3600, 0x0010) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 0x0010) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 20 + } +) -- test.register_coroutine_test( -- "health check coroutine", -- function() @@ -480,6 +797,7 @@ test.register_coroutine_test( test.register_coroutine_test( "configuration version below 1 config response not success", function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") assert(mock_device:get_field("_configuration_version") == nil) test.mock_device.add_test_device(mock_device) @@ -487,6 +805,8 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) test.mock_time.advance_time(5*60 + 1) test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.UNSUPPORTED_ATTRIBUTE)}) @@ -505,6 +825,7 @@ test.register_coroutine_test( test.register_coroutine_test( "configuration version below 1 individual config response records ElectricalMeasurement", function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") assert(mock_device:get_field("_configuration_version") == nil) test.mock_device.add_test_device(mock_device) @@ -512,6 +833,8 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) test.mock_time.advance_time(5*60 + 1) test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, nil, ElectricalMeasurement.attributes.ActivePower.ID, Status.SUCCESS)}) @@ -529,6 +852,7 @@ test.register_coroutine_test( test.register_coroutine_test( "configuration version below 1 individual config response records SimpleMetering", function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") assert(mock_device:get_field("_configuration_version") == nil) test.mock_device.add_test_device(mock_device) @@ -536,6 +860,8 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) test.mock_time.advance_time(5*60 + 1) test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, nil, SimpleMetering.attributes.InstantaneousDemand.ID, Status.SUCCESS)}) @@ -606,6 +932,16 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Color temperature range report test", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive({mock_device.id, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_attr_report(mock_device, 370)}) + test.socket.zigbee:__queue_receive({mock_device.id, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_attr_report(mock_device, 153)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2703, maximum = 6536}))) + end +) + test.register_coroutine_test( "energy meter reset command test", function() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua index b9914ff4af..cc3e86d218 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua @@ -109,7 +109,75 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.commands.MoveToColorTemperature(mock_device, 200, 0) + } + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 20 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua index f0b57744d3..a80bf1dcfc 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -106,15 +106,85 @@ test.register_coroutine_test( OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 20 + } +) + + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnTransitionTime:write(mock_device, 0) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OffTransitionTime:write(mock_device, 0) }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.commands.MoveToColorTemperature(mock_device, 200, 0) + } + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua index 4e63220246..d1bebb68d5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua @@ -38,6 +38,9 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) @@ -69,14 +72,134 @@ test.register_coroutine_test( ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.attributes.CurrentLevel:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 20 } ) @@ -115,7 +238,8 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua new file mode 100644 index 0000000000..3a3c4782ea --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua @@ -0,0 +1,481 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local frameCtrl = require "st.zigbee.zcl.frame_ctrl" +local device_lib = require "st.device" + +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes + +local PRIVATE_CLUSTER_ID = 0xFCCA +local MFG_CODE = 0x1235 +local FINGERPRINTS = require("firstled-io.fingerprints") + +local parent_profile = t_utils.get_profile_definition("switch-button-light-restore-wireless.yml") +local child_switch_profile = t_utils.get_profile_definition("switch-button-wireless.yml") + +local function get_children_amount(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.children + end + end +end + +local function get_button_amount(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.buttons + end + end +end + +local function get_child_profile_name(device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return fingerprint.child_profile + end + end +end + +local function find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) +end +-- ====================== Mock Devices ====================== +local mock_parent = test.mock_device.build_test_zigbee_device({ + profile = parent_profile, + manufacturer = "FIRSTLED", + model = "M4S4BAC", + label = "Mirror Series 4x4 1", + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { id = 1, manufacturer = "FIRSTLED", model = "M4S4BAC", server_clusters = { 0x0004, 0x0006 } } + } +}) + +local mock_children = {} + +for i = 2, 4 do + local name = string.format("%s%d", string.sub("Mirror Series 4x4 1", 0, -2), i) + table.insert(mock_children, test.mock_device.build_test_child_device({ + type = "EDGE_CHILD", + profile = child_switch_profile, + label = name, + device_network_id = string.format("%04X:%02X", mock_parent:get_short_address(), i), + parent_device_id = mock_parent.id, + parent_assigned_child_key = string.format("%02X", i), + vendor_provided_label = name + })) +end + +local function test_init() + test.mock_device.add_test_device(mock_parent) + for _, child in ipairs(mock_children) do + test.mock_device.add_test_device(child) + end +end + +test.set_test_init_function(test_init) + +-- ====================== can_handle ====================== +test.register_coroutine_test("can_handle should return true and handler for matching device", function() + local can_handle = require("firstled-io.can_handle") + local result, handler = can_handle({}, nil, mock_parent) + assert(result == true) + assert(handler ~= nil) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("can_handle should return false for non-matching device", function() + local can_handle = require("firstled-io.can_handle") + local non_match = test.mock_device.build_test_zigbee_device({ + manufacturer = "OTHER", model = "OTHER", profile = parent_profile + }) + local result = can_handle({}, nil, non_match) + assert(result == false) + end, + { + min_api_version = 19 + } +) + +-- ====================== Lifecycle ====================== +test.register_coroutine_test("device_init should set find_child for parent", function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "init"}) + end, + { + min_api_version = 19 + } +) + +-- ====================== device_added ====================== +test.register_coroutine_test("device_added - Zigbee Parent should create children and emit capabilities", function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "added"}) + if mock_parent.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + local children_amount = get_children_amount(mock_parent) + if children_amount >= 2 then + for i = 2, children_amount, 1 do + if find_child(mock_parent, i) == nil then + local name = string.format("%s%d", string.sub(mock_parent.label, 0, -2), i) + local expected_metadata = { + type = "EDGE_CHILD", + label = name, + profile = get_child_profile_name(mock_parent), + parent_device_id = mock_parent.id, + parent_assigned_child_key = string.format("%02X", i), + } + mock_parent:expect_device_create(expected_metadata) + end + end + end + local button_amount = get_button_amount(mock_parent) + if button_amount >= 1 then + for i = children_amount + 1, children_amount + button_amount, 1 do + if find_child(mock_parent, i) == nil then + local name = string.format("%s%d", string.sub(mock_parent.label, 0, -2), i) + local expected_metadata = { + type = "EDGE_CHILD", + label = name, + profile = "button", + parent_device_id = mock_parent.id, + parent_assigned_child_key = string.format("%02X", i), + } + mock_parent:expect_device_create(expected_metadata) + end + end + end + + elseif mock_parent.network_type == "DEVICE_EDGE_CHILD" then + test.socket.capability:__expect_send(mock_parent:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_parent:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + end + end, + { + min_api_version = 19 + } +) + +local function test_device_added_child(ep, name) + test.register_coroutine_test(name, function() + local child = mock_children[ep-1] + test.socket.device_lifecycle:__queue_receive({child.id, "added"}) + + test.socket.capability:__expect_send(child:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + + test.socket.capability:__expect_send(child:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + end, + { + min_api_version = 19 + } +) +end + +for ep = 2, 4 do + test_device_added_child(ep, "test_device_added_child endpoint " .. ep) +end + +-- ====================== Preferences ====================== +test.register_coroutine_test("infoChanged - backlight 0", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { backlight = "0" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - backlight 1", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { backlight = "1" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE, data_types.Uint8, 1) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - backlight 2", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { backlight = "2" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE, data_types.Uint8, 2) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - powerOnStatus 0", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { powerOnStatus = "0" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE, data_types.Uint8, 0) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - powerOnStatus", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { powerOnStatus = "1" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE, data_types.Uint8, 1) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - powerOnStatus 2", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { powerOnStatus = "2" }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE, data_types.Uint8, 2) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - stse.changeToWirelessSwitch true", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { ["stse.changeToWirelessSwitch"] = true }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0002, MFG_CODE, data_types.Boolean, true) + }) + mock_parent:expect_metadata_update({ profile = "switch-button-light-restore-wireless" }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - stse.changeToWirelessSwitch false", function() + test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { ["stse.changeToWirelessSwitch"] = false }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0002, MFG_CODE, data_types.Boolean, false) + }) + mock_parent:expect_metadata_update({ profile = "switch-light-restore-wireless" }) + end, + { + min_api_version = 19 + } +) + +local function test_child_changeToWirelessSwitch_true(ep, name) + test.register_coroutine_test(name, function() + test.socket.device_lifecycle:__queue_receive(mock_children[ep]:generate_info_changed({ preferences = { ["stse.changeToWirelessSwitch"] = true }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0002, MFG_CODE, data_types.Boolean, true):to_endpoint(ep+1) + }) + mock_children[ep]:expect_metadata_update({ profile = "switch-button-wireless" }) + end, + { + min_api_version = 19 + } + ) +end + +local function test_child_changeToWirelessSwitch_false(ep, name) + test.register_coroutine_test(name, function() + test.socket.device_lifecycle:__queue_receive(mock_children[ep]:generate_info_changed({ preferences = { ["stse.changeToWirelessSwitch"] = false }})) + test.socket.zigbee:__expect_send({ mock_parent.id, + cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0002, MFG_CODE, data_types.Boolean, false):to_endpoint(ep+1) + }) + mock_children[ep]:expect_metadata_update({ profile = "switch-wireless" }) + end, + { + min_api_version = 19 + } + ) +end + +for ep = 1, 3 do + test_child_changeToWirelessSwitch_true(ep, "children infoChanged - stse.changeToWirelessSwitch true " .. ep + 1) +end + +for ep = 1, 3 do + test_child_changeToWirelessSwitch_false(ep, "children infoChanged - stse.changeToWirelessSwitch false " .. ep + 1) +end + +-- ====================== Commands ====================== +test.register_message_test("Parent device - On command", { + { channel = "device_lifecycle", direction = "receive", message = { mock_parent.id, "init" }}, + { channel = "capability", direction = "receive", message = { mock_parent.id, { capability = "switch", component = "main", command = "on", args = {} }}}, + { channel = "devices", direction = "send", message = { "register_native_capability_cmd_handler", { device_uuid = mock_parent.id, capability_id = "switch", capability_cmd_id = "on" }}}, + { channel = "zigbee", direction = "send", message = { mock_parent.id, OnOff.server.commands.On(mock_parent):to_endpoint(0x01) }} + }, + { + min_api_version = 19 + } +) + +test.register_message_test("Parent device - Off command", { + { channel = "capability", direction = "receive", message = { mock_parent.id, { capability = "switch", component = "main", command = "off", args = {} }}}, + { channel = "devices", direction = "send", message = { "register_native_capability_cmd_handler", { device_uuid = mock_parent.id, capability_id = "switch", capability_cmd_id = "off" }}}, + { channel = "zigbee", direction = "send", message = { mock_parent.id, OnOff.server.commands.Off(mock_parent):to_endpoint(0x01) }} + }, + { + min_api_version = 19 + } +) + +-- ====================== Attribute Reports ====================== +test.register_coroutine_test( + "OnOff report on parent endpoint", + function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "init"}) + + local report = OnOff.attributes.OnOff:build_test_attr_report(mock_parent, true):from_endpoint(0x01) + test.socket.zigbee:__queue_receive({mock_parent.id, report}) + + test.socket.capability:__expect_send(mock_parent:generate_test_message("main", capabilities.switch.switch.on())) + mock_parent:expect_native_attr_handler_registration("switch", "switch") + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "OnOff report off parent endpoint", + function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "init"}) + + local report = OnOff.attributes.OnOff:build_test_attr_report(mock_parent, false):from_endpoint(0x01) + test.socket.zigbee:__queue_receive({mock_parent.id, report}) + + test.socket.capability:__expect_send(mock_parent:generate_test_message("main", capabilities.switch.switch.off())) + mock_parent:expect_native_attr_handler_registration("switch", "switch") + end, + { + min_api_version = 19 + } +) + +local function test_on_cmd(ep, name) + test.register_message_test(name, { + { channel = "capability", direction = "receive", message = { mock_children[ep].id, { capability = "switch", component = "main", command = "on", args = {} }}}, + { channel = "devices", direction = "send", message = { "register_native_capability_cmd_handler", { device_uuid = mock_children[ep].id, capability_id = "switch", capability_cmd_id = "on" }}}, + { channel = "zigbee", direction = "send", message = { mock_parent.id, OnOff.server.commands.On(mock_parent):to_endpoint(ep+1) }} + }, + { + min_api_version = 19 + }) +end + +local function test_off_cmd(ep, name) + test.register_message_test(name, { + { channel = "capability", direction = "receive", message = { mock_children[ep].id, { capability = "switch", component = "main", command = "off", args = {} }}}, + { channel = "devices", direction = "send", message = { "register_native_capability_cmd_handler", { device_uuid = mock_children[ep].id, capability_id = "switch", capability_cmd_id = "off" }}}, + { channel = "zigbee", direction = "send", message = { mock_parent.id, OnOff.server.commands.Off(mock_parent):to_endpoint(ep+1) }} + }, + { + min_api_version = 19 + }) +end + +local function test_onoff_report_on_cmd(ep, name) + test.register_coroutine_test( + name, + function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "init"}) + + local report = OnOff.attributes.OnOff:build_test_attr_report(mock_parent, true):from_endpoint(ep+1) + test.socket.zigbee:__queue_receive({mock_children[ep].id, report}) + + test.socket.capability:__expect_send(mock_children[ep]:generate_test_message("main", capabilities.switch.switch.on())) + mock_children[ep]:expect_native_attr_handler_registration("switch", "switch") + end, + { + min_api_version = 19 + } + ) +end + +local function test_onoff_report_off_cmd(ep, name) + test.register_coroutine_test( + name, + function() + test.socket.device_lifecycle:__queue_receive({mock_parent.id, "init"}) + + local report = OnOff.attributes.OnOff:build_test_attr_report(mock_parent, false):from_endpoint(ep+1) + test.socket.zigbee:__queue_receive({mock_children[ep].id, report}) + + test.socket.capability:__expect_send(mock_children[ep]:generate_test_message("main", capabilities.switch.switch.off())) + mock_children[ep]:expect_native_attr_handler_registration("switch", "switch") + end, + { + min_api_version = 19 + } + ) +end + +-- ====================== RecallScene ====================== +local function test_recall_scene(ep, name) + test.register_coroutine_test(name, function() + local cmd = Scenes.server.commands.RecallScene.build_test_rx(mock_parent, 0xF0F0, ep) + cmd.body.zcl_header.frame_ctrl = frameCtrl(0x11) + test.socket.zigbee:__queue_receive({ mock_parent.id, cmd }) + test.socket.capability:__expect_send(mock_parent:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + end, + { + min_api_version = 19 + }) +end + +local function test_child_recall_scene(ep, name) + test.register_coroutine_test(name, function() + local cmd = Scenes.server.commands.RecallScene.build_test_rx(mock_parent, 0xF0F0, ep + 1) + cmd.body.zcl_header.frame_ctrl = frameCtrl(0x11) + test.socket.zigbee:__queue_receive({ mock_children[ep].id, cmd }) + test.socket.capability:__expect_send(mock_children[ep]:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + end, + { + min_api_version = 19 + }) +end + +for ep = 1, 1 do + test_recall_scene(ep, "RecallScene on parent endpoint " .. ep) +end + +for ep = 1, 3 do + test_child_recall_scene(ep, "test_child_recall_scene on endpoint " .. ep + 1) +end + +for ep = 1, 3 do + test_on_cmd(ep, "children test_on_cmd on endpoint " .. ep + 1) +end + +for ep = 1, 3 do + test_off_cmd(ep, "children test_off_cmd on endpoint " .. ep + 1) +end + +for ep = 1, 3 do + test_onoff_report_on_cmd(ep, "children test_onoff_report_on_cmd endpoint " .. ep + 1) +end + +for ep = 1, 3 do + test_onoff_report_off_cmd(ep, "children test_onoff_report_off_cmd endpoint " .. ep + 1) +end + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua b/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua index 6e63bba1a9..ebdcae468e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua @@ -20,11 +20,26 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) +local mock_device_eu_em_t = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("switch-power-energy.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LEDVANCE", + model = "PLUG EU EM T", + server_clusters = { 0x0006, 0x0702 } + } + } + } +) + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_eu_em_t) end test.set_test_init_function(test_init) @@ -40,7 +55,7 @@ test.register_coroutine_test( assert(mock_device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 100) end, { - min_api_version = 15 + min_api_version = 17 } ) @@ -55,7 +70,37 @@ test.register_coroutine_test( assert(mock_device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 1000) end, { - min_api_version = 15 + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Device init should set default multiplier and divisor only when not already set - PLUG EU EM T", + function() + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) == nil) + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == nil) + test.socket.device_lifecycle:__queue_receive({ mock_device_eu_em_t.id, "init" }) + test.wait_for_events() + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) == 1) + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 100) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Device init should preserve device-reported multiplier and divisor - PLUG EU EM T", + function() + mock_device_eu_em_t:set_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY, 5, {persist = true}) + mock_device_eu_em_t:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + test.socket.device_lifecycle:__queue_receive({ mock_device_eu_em_t.id, "init" }) + test.wait_for_events() + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) == 5) + assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 1000) + end, + { + min_api_version = 17 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua index f025655b26..4a83d8b920 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua @@ -81,6 +81,18 @@ test.register_coroutine_test( ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 16) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) @@ -90,7 +102,71 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.CurrentHue:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 16) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua index af64b089a4..80241b4ca9 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua @@ -4,6 +4,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -69,14 +70,136 @@ test.register_coroutine_test( ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.attributes.CurrentLevel:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 20 } ) @@ -115,7 +238,8 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) @@ -149,4 +273,51 @@ test.register_coroutine_test( } ) +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 +local REPORTED_MIRED_MIN = 160 +local REPORTED_MIRED_MAX = 370 + +test.register_coroutine_test( + "Step Color Temperature command with device-reported mired range test", + function() + -- Report non-default range values to verify subsequent step commands do not use defaults. + test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_attr_report(mock_device, REPORTED_MIRED_MAX)}) + test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_attr_report(mock_device, REPORTED_MIRED_MIN)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2703, maximum = 6250}))) + test.wait_for_events() + + test.socket.capability:__queue_receive({mock_device.id, { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } }) + mock_device:expect_native_cmd_handler_registration("statelessColorTemperatureStep", "stepColorTemperatureByPercent") + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 42, TRANSITION_TIME, REPORTED_MIRED_MIN, REPORTED_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "Step Level command test", + function() + test.socket.capability:__queue_receive({mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } }) + mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel") + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + ) + test.wait_for_events() + end, + { + min_api_version = 19 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua index 7466edd1e2..8a0db9d65c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua @@ -38,6 +38,9 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) @@ -69,14 +72,135 @@ test.register_coroutine_test( ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.attributes.CurrentLevel:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed", + min_api_version = 20 } ) @@ -115,7 +239,8 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua index 2ada61a3e6..f6842b077f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua @@ -11,6 +11,12 @@ local OnOff = clusters.OnOff local Level = clusters.Level local ColorControl = clusters.ColorControl +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 +local DEFAULT_MIRED_MIN = 154 +local DEFAULT_MIRED_MAX = 370 + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("color-temp-bulb.yml"), fingerprinted_endpoint_id = 0x01, @@ -42,9 +48,26 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -56,6 +79,8 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) test.wait_for_events() test.mock_time.advance_time(50000) @@ -67,10 +92,55 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "polling") end, - min_api_version = 17 + min_api_version = 20 } ) +test.register_coroutine_test( + "ZLL periodic poll should occur", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.wait_for_events() + + test.mock_time.advance_time(50000) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.wait_for_events() + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device) + test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "polling") + end, + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_coroutine_test( + "Switch command on should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, + { + min_api_version = 20 + } +) + test.register_coroutine_test( "Switch command on should be handled", function() @@ -86,7 +156,8 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 17, + max_api_version = 19 } ) @@ -103,9 +174,31 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Switch command off should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device)}) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -122,9 +215,31 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "SwitchLevel command setLevel should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = {50} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end + test.socket.zigbee:__expect_send({ mock_device.id, Level.commands.MoveToLevelWithOnOff(mock_device, math.floor(50 / 100.0 * 254), 0xFFFF)}) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -142,9 +257,101 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, + { + min_api_version = 20 + } +) + +test.register_coroutine_test( + "ColorTemperature command setColorTemperature should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + end, + { + min_api_version = 17, + max_api_version = 19 + } +) + +test.register_coroutine_test( + "StatelessColorTemperatureStep stepColorTemperatureByPercent should trigger delayed refresh on ZLL device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } }) + mock_device:expect_native_cmd_handler_registration("statelessColorTemperatureStep", "stepColorTemperatureByPercent") + test.socket.zigbee:__expect_send({ + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 43, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, + { + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Rapid StatelessSwitchLevelStep stepLevel commands should cancel and recreate delayed refresh timer", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } }) + mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel") + test.socket.zigbee:__expect_send({ + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + test.wait_for_events() + test.mock_time.advance_time(1) + + -- Second step command: cancels timer #1, creates timer #2 + test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } }) + mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel") + test.socket.zigbee:__expect_send({ + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + test.wait_for_events() + test.mock_time.advance_time(1) + -- now, nothing should happen since the first timer was cancelled and the second timer has not yet reached its 2s delay + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 20 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua index 19d91d697a..ec10a66a62 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua @@ -10,6 +10,10 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff local Level = clusters.Level +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("on-off-level.yml"), fingerprinted_endpoint_id = 0x01, @@ -178,4 +182,27 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "StatelessSwitchLevelStep stepLevel should trigger delayed refresh on ZLL device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } }) + mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel") + test.socket.zigbee:__expect_send({ + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + end, + { + min_api_version = 19 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua index 68f8cfdcc2..4e2ad43b06 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua @@ -40,6 +40,9 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) @@ -83,10 +86,82 @@ test.register_coroutine_test( ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 16) } ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMaxMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTempPhysicalMinMireds:configure_reporting(mock_device, 1, 43200, 1) + } + ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Level.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ColorControl.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.ColorTemperatureMireds:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.CurrentHue:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 16) + } + ) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -100,9 +175,28 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -116,6 +210,8 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) test.wait_for_events() test.mock_time.advance_time(5*60) @@ -127,7 +223,33 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(5*60, "interval", "polling") end, - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "ZLL periodic poll should occur", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.wait_for_events() + + test.mock_time.advance_time(5*60) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.wait_for_events() + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device) + test.timer.__create_and_queue_test_time_advance_timer(5*60, "interval", "polling") + end, + min_api_version = 17, + max_api_version = 19 } ) @@ -149,9 +271,38 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Capability 'switch' command 'on' should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -173,9 +324,38 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Capability 'switch' command 'off' should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -197,9 +377,38 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "Capability 'switchLevel' command 'setLevel' on should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end + + test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + + end, + { + min_api_version = 17, + max_api_version = 19 } ) @@ -222,9 +431,39 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) }) + end, { - min_api_version = 17 + min_api_version = 20 + } +) + +test.register_coroutine_test( + "ColorTemperature command setColorTemperature should be handled", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) + + test.wait_for_events() + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) + + end, + { + min_api_version = 17, + max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua index 1e0e7eb26e..5ad97bfb29 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua @@ -102,7 +102,7 @@ local wallheroswitch = { NAME = "Zigbee Wall Hero Switch", lifecycle_handlers = { added = device_added, - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), infoChanged = device_info_changed }, zigbee_handlers = { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua index dc37bf1181..869678ef24 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua @@ -41,7 +41,7 @@ local zigbee_dimmer_power_energy_handler = { } }, lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), doConfigure = do_configure, }, can_handle = require("zigbee-dimmer-power-energy.can_handle"), diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index dc8629c57b..880e947163 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -48,7 +48,7 @@ end local zigbee_dimming_light = { NAME = "Zigbee Dimming Light", lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), added = device_added, doConfigure = do_configure }, diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua index b1d10f94a6..f88c6396f5 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua @@ -52,7 +52,7 @@ local zigbee_dual_metering_switch = { } }, lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), added = device_added }, can_handle = require("zigbee-dual-metering-switch.can_handle"), diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 53a7e1eb99..1845878f19 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -41,7 +41,7 @@ local zigbee_metering_plug_power_conumption_report = { } }, lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), + init = configurations.reconfig_wrapper(device_init), doConfigure = do_configure }, can_handle = require("zigbee-metering-plug-power-consumption-report.can_handle"), diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua index d277d36967..f393f61eea 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua @@ -14,5 +14,7 @@ return { { mfr = "SALUS", model = "SX885ZB" }, { mfr = "AduroSmart Eria", model = "AD-SmartPlug3001" }, { mfr = "AduroSmart Eria", model = "BPU3" }, - { mfr = "AduroSmart Eria", model = "BDP3001" } + { mfr = "AduroSmart Eria", model = "BDP3001" }, + { mfr = "LEDVANCE", model = "PLUG COMPACT EU EM T" }, + { mfr = "LEDVANCE", model = "PLUG EU EM T" } } diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/can_handle.lua new file mode 100644 index 0000000000..4ecb5ab271 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local FINGERPRINTS = require("zigbee-switch-power.ledvance-metering-plug.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-switch-power.ledvance-metering-plug") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/fingerprints.lua new file mode 100644 index 0000000000..50542e69ea --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/fingerprints.lua @@ -0,0 +1,7 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "LEDVANCE", model = "PLUG COMPACT EU EM T" }, + { mfr = "LEDVANCE", model = "PLUG EU EM T" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/init.lua new file mode 100644 index 0000000000..55e703a6d6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/ledvance-metering-plug/init.lua @@ -0,0 +1,23 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local zigbee_constants = require "st.zigbee.constants" + +local function device_init(driver, device) + if device:get_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY) == nil then + device:set_field(zigbee_constants.SIMPLE_METERING_MULTIPLIER_KEY, 1, {persist = true}) + end + if device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == nil then + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 100, {persist = true}) + end +end + +local ledvance_metering_plug = { + NAME = "LEDVANCE Metering Plug", + lifecycle_handlers = { + init = device_init + }, + can_handle = require("zigbee-switch-power.ledvance-metering-plug.can_handle") +} + +return ledvance_metering_plug diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua index 340e1f27c6..0c1ba5eeef 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua @@ -5,5 +5,6 @@ local lazy_load = require "lazy_load_subdriver" return { lazy_load("zigbee-switch-power.aurora-relay"), - lazy_load("zigbee-switch-power.vimar") + lazy_load("zigbee-switch-power.vimar"), + lazy_load("zigbee-switch-power.ledvance-metering-plug") } diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index 3478b39bd5..814e0f70ce 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -3,37 +3,65 @@ local device_lib = require "st.device" local clusters = require "st.zigbee.zcl.clusters" +local configurationMap = require "configurations" -local function set_up_zll_polling(driver, device) - local INFREQUENT_POLL_COUNTER = "_infrequent_poll_counter" - local function poll() - local infrequent_counter = device:get_field(INFREQUENT_POLL_COUNTER) or 1 - if infrequent_counter == 12 then - -- do a full refresh once an hour - device:refresh() - infrequent_counter = 0 - else - -- Read On/Off every poll - for _, ep in pairs(device.zigbee_endpoints) do - if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then - device:send(clusters.OnOff.attributes.OnOff:read(device):to_endpoint(ep.id)) - end +local INFREQUENT_POLL_COUNTER = "_infrequent_poll_counter" +local ZLL_POLL_TIMER = "_zll_poll_timer" + +local function do_zll_poll(device) + if device == nil or type(device.get_field) ~= "function" then + return + end + + local infrequent_counter = device:get_field(INFREQUENT_POLL_COUNTER) or 1 + if infrequent_counter == 12 then + -- do a full refresh once an hour + device:refresh() + infrequent_counter = 0 + else + -- Read On/Off every poll + for _, ep in pairs(device.zigbee_endpoints) do + if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then + device:send(clusters.OnOff.attributes.OnOff:read(device):to_endpoint(ep.id)) end - infrequent_counter = infrequent_counter + 1 end - device:set_field(INFREQUENT_POLL_COUNTER, infrequent_counter) + infrequent_counter = infrequent_counter + 1 end + device:set_field(INFREQUENT_POLL_COUNTER, infrequent_counter) +end +local function set_up_zll_polling(driver, device) -- only set this up for non-child devices - if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then - device.thread:call_on_schedule(5 * 60, poll, "zll_polling") + if device.network_type ~= device_lib.NETWORK_TYPE_ZIGBEE then + return + end + + -- should never happen, but defensive check + local existing_timer = device:get_field(ZLL_POLL_TIMER) + if existing_timer ~= nil then + device.thread:cancel_timer(existing_timer) + end + + local timer = device.thread:call_on_schedule(5 * 60, function() + do_zll_poll(device) + end, "zll_polling") + + device:set_field(ZLL_POLL_TIMER, timer) +end + +local function remove_zll_polling(driver, device) + local existing_timer = device:get_field(ZLL_POLL_TIMER) + if existing_timer ~= nil then + device.thread:cancel_timer(existing_timer) + device:set_field(ZLL_POLL_TIMER, nil) end end local ZLL_polling = { NAME = "ZLL Polling", lifecycle_handlers = { - init = set_up_zll_polling + init = configurationMap.reconfig_wrapper(set_up_zll_polling), + removed = remove_zll_polling }, can_handle = require("zll-polling.can_handle"), } diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index b1766e7892..a72b3c9107 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -354,6 +354,7 @@ local zigbee_thermostat_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_thermostat_driver, zigbee_thermostat_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua new file mode 100644 index 0000000000..f16f04858c --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ezex_can_handle(opts, driver, device, ...) + local clusters = require "st.zigbee.zcl.clusters" + if device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) then + return true, require("ezex") + end + return false +end + +return ezex_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua index 47ced66806..c32fe60bac 100644 --- a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -71,9 +61,7 @@ local ezex_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) - end + can_handle = require("ezex.can_handle"), } return ezex_valve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 6d355de8ec..717012e2bb 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -51,11 +41,9 @@ local zigbee_valve_driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("sinope"), - require("ezex") - }, + sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_valve_driver_template, zigbee_valve_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua new file mode 100644 index 0000000000..f533d120e0 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function sinope_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Sinope Technologies" then + return true, require("sinope") + end + return false +end + +return sinope_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua index 6a0075cd33..0c6994ad00 100644 --- a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -22,7 +12,9 @@ local PowerConfiguration = clusters.PowerConfiguration local function device_init(driver, device) battery_defaults.use_battery_voltage_handling(device) -- according to the DTH, this attribute cannot be configured for reporting - device.thread:call_on_schedule(900, function() device:send(PowerConfiguration.attributes.BatteryVoltage:read()) end) + device.thread:call_on_schedule(900, function() + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + end) end local function battery_voltage_handler(driver, device, command) @@ -59,9 +51,7 @@ local sinope_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Sinope Technologies" - end + can_handle = require("sinope.can_handle"), } return sinope_valve diff --git a/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua new file mode 100644 index 0000000000..258579c7fb --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("sinope"), + lazy_load_if_possible("ezex"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua index 81a3302eaa..d9db636eab 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua index 324001b197..9ba28f751e 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua index 60090c49f2..72038fdb37 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua index 4ded4e195c..6f4b879108 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua @@ -81,6 +81,7 @@ local zigbee_water_driver_template = { ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_water_driver_template, diff --git a/drivers/SmartThings/zigbee-watering-kit/src/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/init.lua index 7dd35e6f09..7e23c2fef9 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/init.lua @@ -15,6 +15,7 @@ local zigbee_water_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_water_driver_template, zigbee_water_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua new file mode 100644 index 0000000000..85db6c0447 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("HOPOsmart.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("HOPOsmart") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua index 1f2950b2f1..b0394aa3fe 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua new file mode 100644 index 0000000000..abf5e54ae5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "HOPOsmart", model = "A2230011" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua index 5e3f002ec0..3267eefc33 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua @@ -1,34 +1,13 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local custom_clusters = require "HOPOsmart/custom_clusters" local cluster_base = require "st.zigbee.cluster_base" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "HOPOsmart", model = "A2230011" } -} -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function send_read_attr_request(device, cluster, attr) device:send( @@ -83,7 +62,7 @@ local HOPOsmart_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("HOPOsmart.can_handle"), } return HOPOsmart_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua new file mode 100644 index 0000000000..d6907690c3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("VIVIDSTORM.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("VIVIDSTORM") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua index 6f266a474e..8efbc0e654 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua new file mode 100644 index 0000000000..ff1bbcb00d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua index ef36757490..106115edee 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -22,18 +12,7 @@ local MOST_RECENT_SETLEVEL = "windowShade_recent_setlevel" local TIMER = "liftPercentage_timer" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } -} -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function send_read_attr_request(device, cluster, attr) device:send( @@ -156,7 +135,7 @@ local screen_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("VIVIDSTORM.can_handle"), } return screen_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua new file mode 100644 index 0000000000..7eb40a7c10 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function curtain_driver_e1_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.agl001" then + return true, require("aqara.curtain-driver-e1") + end + return false +end + +return curtain_driver_e1_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 6fe895ca7d..03e651b679 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -224,9 +214,7 @@ local aqara_curtain_driver_e1_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.agl001" - end + can_handle = require("aqara.curtain-driver-e1.can_handle"), } return aqara_curtain_driver_e1_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..8ad05530a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.curtain" }, + { mfr = "LUMI", model = "lumi.curtain.v1" }, + { mfr = "LUMI", model = "lumi.curtain.aq2" }, + { mfr = "LUMI", model = "lumi.curtain.agl001" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index 87505d2e40..1b66236038 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -27,21 +31,7 @@ local PREF_SOFT_TOUCH_ON = "\x00\x08\x00\x00\x00\x00\x00" local APPLICATION_VERSION = "application_version" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.curtain" }, - { mfr = "LUMI", model = "lumi.curtain.v1" }, - { mfr = "LUMI", model = "lumi.curtain.aq2" }, - { mfr = "LUMI", model = "lumi.curtain.agl001" } -} -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function window_shade_level_cmd(driver, device, command) aqara_utils.shade_level_cmd(driver, device, command) @@ -217,12 +207,8 @@ local aqara_window_treatment_handler = { } } }, - sub_drivers = { - require("aqara.roller-shade"), - require("aqara.curtain-driver-e1"), - require("aqara.version") - }, - can_handle = is_aqara_products + sub_drivers = require("aqara.sub_drivers"), + can_handle = require("aqara.can_handle"), } return aqara_window_treatment_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua new file mode 100644 index 0000000000..2e91fa090d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function roller_shade_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.aq2" then + return true, require("aqara.roller-shade") + end + return false +end + +return roller_shade_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 12b84eb0c8..909b65dd3e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -1,141 +1,142 @@ -local capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" -local data_types = require "st.zigbee.data_types" -local aqara_utils = require "aqara/aqara_utils" -local window_treatment_utils = require "window_treatment_utils" - -local Basic = clusters.Basic -local WindowCovering = clusters.WindowCovering - -local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseRollerShadeDir = "stse.reverseRollerShadeDir" -local shadeRotateState = capabilities["stse.shadeRotateState"] -local setRotateStateCommandName = "setRotateState" - -local MULTISTATE_CLUSTER_ID = 0x0013 -local MULTISTATE_ATTRIBUTE_ID = 0x0055 -local ROTATE_UP_VALUE = 0x0004 -local ROTATE_DOWN_VALUE = 0x0005 - - -local function window_shade_level_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - aqara_utils.shade_level_cmd(driver, device, command) - end -end - -local function window_shade_open_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) - end -end - -local function window_shade_close_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) - end -end - -local function set_rotate_command_handler(driver, device, command) - device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI - - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - local state = command.args.state - if state == "rotateUp" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - elseif state == "rotateDown" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - end - end -end - -local function shade_state_report_handler(driver, device, value, zb_rx) - aqara_utils.emit_shade_event_by_state(device, value) -end - -local function pref_report_handler(driver, device, value, zb_rx) - -- initializedState - local initialized = string.byte(value.value, 3) & 0xFF - device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or - initializedStateWithGuide.initializedStateWithGuide.notInitialized()) -end - -local function device_info_changed(driver, device, event, args) - if device.preferences ~= nil then - local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir] - if reverseRollerShadeDirPrefValue ~= nil and - reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir] then - local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, raw_value)) - end - end -end - -local function device_added(driver, device) - device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) - window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) - device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) - device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) - - device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, - aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) - - -- Initial default settings - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) -end - -local aqara_roller_shade_handler = { - NAME = "Aqara Roller Shade Handler", - lifecycle_handlers = { - added = device_added, - infoChanged = device_info_changed - }, - capability_handlers = { - [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd - }, - [capabilities.windowShade.ID] = { - [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, - [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, - }, - [shadeRotateState.ID] = { - [setRotateStateCommandName] = set_rotate_command_handler - } - }, - zigbee_handlers = { - attr = { - [Basic.ID] = { - [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, - [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.aq2" - end -} - -return aqara_roller_shade_handler +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" +local data_types = require "st.zigbee.data_types" +local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering + +local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] +local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] +local shadeRotateState = capabilities["stse.shadeRotateState"] +local setRotateStateCommandName = "setRotateState" + +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0004 +local ROTATE_DOWN_VALUE = 0x0005 + + +local function window_shade_level_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + aqara_utils.shade_level_cmd(driver, device, command) + end +end + +local function window_shade_open_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) + end +end + +local function window_shade_close_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) + end +end + +local function set_rotate_command_handler(driver, device, command) + device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI + + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + local state = command.args.state + if state == "rotateUp" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + elseif state == "rotateDown" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + end + end +end + +local function shade_state_report_handler(driver, device, value, zb_rx) + aqara_utils.emit_shade_event_by_state(device, value) +end + +local function pref_report_handler(driver, device, value, zb_rx) + -- initializedState + local initialized = string.byte(value.value, 3) & 0xFF + device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or + initializedStateWithGuide.initializedStateWithGuide.notInitialized()) +end + +local function device_info_changed(driver, device, event, args) + if device.preferences ~= nil then + local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] + if reverseRollerShadeDirPrefValue ~= nil and + reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then + local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, raw_value)) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) + + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) + + -- Initial default settings + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) +end + +local aqara_roller_shade_handler = { + NAME = "Aqara Roller Shade Handler", + lifecycle_handlers = { + added = device_added, + infoChanged = device_info_changed + }, + capability_handlers = { + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, + [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, + }, + [shadeRotateState.ID] = { + [setRotateStateCommandName] = set_rotate_command_handler + } + }, + zigbee_handlers = { + attr = { + [Basic.ID] = { + [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, + [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler + } + } + }, + can_handle = require("aqara.roller-shade.can_handle"), +} + +return aqara_roller_shade_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..297f29d970 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara.roller-shade"), + lazy_load_if_possible("aqara.curtain-driver-e1"), + lazy_load_if_possible("aqara.version"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua new file mode 100644 index 0000000000..5d9f7f0135 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function version_can_handle(opts, driver, device) + local APPLICATION_VERSION = "application_version" + local softwareVersion = device:get_field(APPLICATION_VERSION) + if softwareVersion and softwareVersion ~= 34 then + return true, require("aqara.version") + end + return false +end + +return version_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua index 10182ee928..ae1887d79a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua @@ -1,9 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local WindowCovering = clusters.WindowCovering -local APPLICATION_VERSION = "application_version" - local function shade_level_report_legacy_handler(driver, device, value, zb_rx) -- not implemented end @@ -17,10 +18,7 @@ local aqara_window_treatment_version_handler = { } } }, - can_handle = function(opts, driver, device) - local softwareVersion = device:get_field(APPLICATION_VERSION) - return softwareVersion and softwareVersion ~= 34 - end + can_handle = require("aqara.version.can_handle"), } return aqara_window_treatment_version_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua new file mode 100644 index 0000000000..828eb170fa --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_axis_gear_version = function(opts, driver, device) + local SOFTWARE_VERSION = "software_version" + local MIN_WINDOW_COVERING_VERSION = 1093 + local version = device:get_field(SOFTWARE_VERSION) or 0 + + if version >= MIN_WINDOW_COVERING_VERSION then + return true, require("axis.axis_version") + end + return false +end + +return is_axis_gear_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua index c0682d73c0..cbb2930bc3 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local window_shade_utils = require "window_shade_utils" @@ -22,18 +12,8 @@ local Level = zcl_clusters.Level local PowerConfiguration = zcl_clusters.PowerConfiguration local WindowCovering = zcl_clusters.WindowCovering -local SOFTWARE_VERSION = "software_version" -local MIN_WINDOW_COVERING_VERSION = 1093 local DEFAULT_LEVEL = 0 -local is_axis_gear_version = function(opts, driver, device) - local version = device:get_field(SOFTWARE_VERSION) or 0 - - if version >= MIN_WINDOW_COVERING_VERSION then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -141,7 +121,7 @@ local axis_handler_version = { } } }, - can_handle = is_axis_gear_version, + can_handle = require("axis.axis_version.can_handle"), } return axis_handler_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua new file mode 100644 index 0000000000..049e47acb5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + if device:get_manufacturer() == "AXIS" then + return true, require("axis") + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua index 612dd450a5..ec7c96b975 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" @@ -26,12 +17,6 @@ local WindowCovering = zcl_clusters.WindowCovering local SOFTWARE_VERSION = "software_version" local DEFAULT_LEVEL = 0 -local is_zigbee_window_shade = function(opts, driver, device) - if device:get_manufacturer() == "AXIS" then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -151,8 +136,8 @@ local axis_handler = { added = device_added, doConfigure = do_configure, }, - sub_drivers = { require("axis.axis_version") }, - can_handle = is_zigbee_window_shade, + sub_drivers = require("axis.sub_drivers"), + can_handle = require("axis.can_handle"), } return axis_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua new file mode 100644 index 0000000000..e3ea740478 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("axis.axis_version") +} diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua new file mode 100644 index 0000000000..32457dde8a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("feibit.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("feibit") + end + end + + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua new file mode 100644 index 0000000000..95781ff992 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua index e0fd17219e..1e97fbc4ce 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -19,20 +9,7 @@ local window_shade_defaults = require "st.zigbee.defaults.windowShade_defaults" local device_management = require "st.zigbee.device_management" local Level = zcl_clusters.Level -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, -} - -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function set_shade_level(device, value, component) local level = math.floor(value / 100.0 * 254) @@ -87,7 +64,7 @@ local feibit_handler = { lifecycle_handlers = { doConfigure = do_configure, }, - can_handle = is_zigbee_window_shade, + can_handle = require("feibit.can_handle"), } return feibit_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua new file mode 100644 index 0000000000..65f1a6dc75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function hanssem_can_handle(opts, driver, device, ...) + if device:get_model() == "TS0601" then + return true, require("hanssem") + end + return false +end + +return hanssem_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua index be956d79b2..41b4eb1cac 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2021-2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- -- Based on https://github.com/iquix/ST-Edge-Driver/blob/master/tuya-window-shade/src/init.lua -- Copyright 2021-2022 Jaewon Park (iquix) @@ -241,9 +244,7 @@ local hanssem_window_treatment = { added = device_added, infoChanged = device_info_changed }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TS0601" - end + can_handle = require("hanssem.can_handle"), } -return hanssem_window_treatment \ No newline at end of file +return hanssem_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 3403b3d528..0a093645f8 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -46,30 +36,19 @@ local zigbee_window_treatment_driver_template = { capabilities.powerSource, capabilities.battery }, - sub_drivers = { - require("vimar"), - require("aqara"), - require("feibit"), - require("somfy"), - require("invert-lift-percentage"), - require("rooms-beautiful"), - require("axis"), - require("yoolax"), - require("hanssem"), - require("screen-innovations"), - require("VIVIDSTORM"), - require("HOPOsmart")}, - lifecycle_handlers = { - init = init_handler, - added = added_handler - }, capability_handlers = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_shade_utils.set_preset_position_cmd, [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_utils.window_shade_preset_cmd, } }, + lifecycle_handlers = { + init = init_handler, + added = added_handler + }, + sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_window_treatment_driver_template, zigbee_window_treatment_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua new file mode 100644 index 0000000000..69cfd4393a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function invert_lift_percentage_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "IKEA of Sweden" or + device:get_manufacturer() == "Smartwings" or + device:get_manufacturer() == "Insta GmbH" + then + return true, require("invert-lift-percentage") + end + return false +end + +return invert_lift_percentage_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua index 19532bc847..b586459b9a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -97,11 +87,7 @@ local ikea_window_treatment = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "IKEA of Sweden" or - device:get_manufacturer() == "Smartwings" or - device:get_manufacturer() == "Insta GmbH" - end + can_handle = require("invert-lift-percentage.can_handle"), } return ikea_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua new file mode 100644 index 0000000000..6bc25d2f91 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("rooms-beautiful.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("rooms-beautiful") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua new file mode 100644 index 0000000000..71ece32b8e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Rooms Beautiful", model = "C001" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua index fc4883aa7f..bb868a8716 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -21,22 +11,11 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local OnOff = zcl_clusters.OnOff local WindowCovering = zcl_clusters.WindowCovering -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Rooms Beautiful", model = "C001" } -} local INVERT_CLUSTER = 0xFC00 local INVERT_CLUSTER_ATTRIBUTE = 0x0000 local PREV_TIME = "shadeLevelCmdTime" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function invert_preference_handler(device) local window_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 @@ -129,7 +108,7 @@ local rooms_beautiful_handler = { init = battery_defaults.build_linear_voltage_init(2.5, 3.0), infoChanged = info_changed }, - can_handle = is_zigbee_window_shade, + can_handle = require("rooms-beautiful.can_handle"), } return rooms_beautiful_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua new file mode 100644 index 0000000000..df291c2612 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function screen_innovations_can_handle(opts, driver, device, ...) + if device:get_model() == "WM25/L-Z" then + return true, require("screen-innovations") + end + return false +end + +return screen_innovations_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua index 868e92af56..49397a5369 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- require st provided libraries local capabilities = require "st.capabilities" @@ -173,9 +163,7 @@ local screeninnovations_roller_shade_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "WM25/L-Z" - end + can_handle = require("screen-innovations.can_handle"), } -- return the handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua new file mode 100644 index 0000000000..27a6c83ca3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("somfy.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("somfy") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua new file mode 100644 index 0000000000..ce6094564c --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, + { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, + { mfr = "SOMFY", model = "Sonesse 40 Roller" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua index ffc9541b64..da416ba9ea 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -20,23 +10,10 @@ local WindowCovering = zcl_clusters.WindowCovering local GLYDEA_MOVE_THRESHOLD = 3 -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, - { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, - { mfr = "SOMFY", model = "Sonesse 40 Roller" } -} local MOVE_LESS_THAN_THRESHOLD = "_sameLevelEvent" local FINAL_STATE_POLL_TIMER = "_finalStatePollTimer" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function overwrite_existing_timer_if_needed(device, new_timer) local old_timer = device:get_field(FINAL_STATE_POLL_TIMER) @@ -132,7 +109,7 @@ local somfy_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("somfy.can_handle"), } return somfy_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua new file mode 100644 index 0000000000..959c8d8c22 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("vimar"), + lazy_load_if_possible("aqara"), + lazy_load_if_possible("feibit"), + lazy_load_if_possible("somfy"), + lazy_load_if_possible("invert-lift-percentage"), + lazy_load_if_possible("rooms-beautiful"), + lazy_load_if_possible("axis"), + lazy_load_if_possible("yoolax"), + lazy_load_if_possible("hanssem"), + lazy_load_if_possible("screen-innovations"), + lazy_load_if_possible("VIVIDSTORM"), + lazy_load_if_possible("HOPOsmart"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua index 891a1517df..26a08b1a84 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua index bfc5677384..f505e0d661 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua index f62f87ddf9..71dc5c81a4 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua index f715f15ee5..ed01a87249 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua index 931f274516..ecf2586a5c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua index 8104b0a628..08c71ec85a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua index a1c7dd0b9e..0177c71467 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua index b2f849b29b..3e1e013c38 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua index 45383f61db..fb63dd121d 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua index e087be1fa3..01f2e98556 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index efe268a2af..15cae0e344 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -1,16 +1,6 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -392,4 +382,4 @@ test.register_coroutine_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua index 6f2ef04a6c..9f80bfdc80 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua index a5e0d3d7d5..a929eb4e98 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -471,4 +461,4 @@ test.register_coroutine_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua index 194465c36e..e772b46bb9 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua index cd3eae500e..40e10d6b3b 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua new file mode 100644 index 0000000000..1d72817eae --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("vimar.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("vimar") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua new file mode 100644 index 0000000000..ea7f4cd3bf --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Vimar", model = "Window_Cov_v1.0" }, + { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua index 9fa928645a..dd5ea15aed 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -27,20 +17,8 @@ local windowShade = capabilities.windowShade.windowShade local VIMAR_SHADES_OPENING = "_vimarShadesOpening" local VIMAR_SHADES_CLOSING = "_vimarShadesClosing" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Vimar", model = "Window_Cov_v1.0" }, - { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } -} -- UTILS to check manufacturer details -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- ATTRIBUTE HANDLER FOR CurrentPositionLiftPercentage local function current_position_attr_handler(driver, device, value, zb_rx) @@ -176,7 +154,7 @@ local vimar_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_zigbee_window_shade, + can_handle = require("vimar.can_handle"), } return vimar_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua index 262e549c2d..f3e09c20a6 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -41,4 +31,4 @@ utils.set_preset_position_cmd = function(driver, device, command) device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) end -return utils \ No newline at end of file +return utils diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua index 2f20ff2b4f..5b2ec304e6 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local window_treatment_utils = {} diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua new file mode 100644 index 0000000000..006fa3e1bb --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_yoolax_window_shade(opts, driver, device) + local FINGERPRINTS = require("yoolax.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yoolax") + end + end + return false +end + +return is_yoolax_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua new file mode 100644 index 0000000000..30e0dd4c62 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment + { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment +} + +return YOOLAX_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua index 46cb33fed2..5a593cdf2c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -24,19 +14,7 @@ local device_management = require "st.zigbee.device_management" local LEVEL_UPDATE_TIMEOUT = "__level_update_timeout" local MOST_RECENT_SETLEVEL = "__most_recent_setlevel" -local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment - { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment -} -local function is_yoolax_window_shade(opts, driver, device) - for _, fingerprint in ipairs(YOOLAX_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function default_response_handler(driver, device, zb_message) local is_success = zb_message.body.zcl_body.status.value @@ -160,7 +138,7 @@ local yoolax_window_shade = { } }, }, - can_handle = is_yoolax_window_shade + can_handle = require("yoolax.can_handle"), } return yoolax_window_shade diff --git a/drivers/SmartThings/zwave-bulb/src/init.lua b/drivers/SmartThings/zwave-bulb/src/init.lua index 58e2f32a7e..fd6c9a9b8f 100644 --- a/drivers/SmartThings/zwave-bulb/src/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/init.lua @@ -21,6 +21,7 @@ local driver_template = { capabilities.powerMeter }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_cmds_enabled = true}) diff --git a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua deleted file mode 100644 index 0204b7b2d5..0000000000 --- a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-button/src/configurations.lua b/drivers/SmartThings/zwave-button/src/configurations.lua index 5dc1b96e0f..2c93b5075c 100644 --- a/drivers/SmartThings/zwave-button/src/configurations.lua +++ b/drivers/SmartThings/zwave-button/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { AEOTEC_NANOMOTE_ONE = { diff --git a/drivers/SmartThings/zwave-button/src/init.lua b/drivers/SmartThings/zwave-button/src/init.lua index b369197a5b..18469ca56b 100644 --- a/drivers/SmartThings/zwave-button/src/init.lua +++ b/drivers/SmartThings/zwave-button/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -41,10 +31,8 @@ local driver_template = { lifecycle_handlers = { added = added_handler, }, - sub_drivers = { - require("zwave-multi-button"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-button/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-button/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-button/src/sub_drivers.lua b/drivers/SmartThings/zwave-button/src/sub_drivers.lua new file mode 100644 index 0000000000..64a9813468 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-multi-button"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua index 6876b7026f..88bf890686 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua index 4d8be4dad2..7df9c6cada 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua index f5e16ba158..664d048010 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua index e03471594f..5418917ee5 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua index 07132534c5..b88a85a800 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua new file mode 100644 index 0000000000..5a2fec217c --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_keyfob(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.aeotec-keyfob.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.aeotec-keyfob") + end + end + return false +end + +return can_handle_aeotec_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua new file mode 100644 index 0000000000..dd6a9c8219 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US + {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU + {mfr = 0x0086, prod = 0x0001, model = 0x0026} -- Aeotec Panic Button +} + +return ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua index c8b655bff2..13096c0579 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua @@ -1,37 +1,11 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) --- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({ version=1 }) -local ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US - {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU - {mfr = 0x0086, prod = 0x0001, model = 0x0026} -- Aeotec Panic Button -} - -local function can_handle_aeotec_keyfob(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local do_configure = function(self, device) device:refresh() device:send(Configuration:Set({ configuration_value = 1, parameter_number = 250, size = 1 })) @@ -43,7 +17,8 @@ local aeotec_keyfob = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aeotec_keyfob, + can_handle = require("zwave-multi-button.aeotec-keyfob.can_handle"), + shared_device_thread_enabled = true, } return aeotec_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua new file mode 100644 index 0000000000..fb39fa8f70 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_minimote(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.aeotec-minimote.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.aeotec-minimote") + end + end + return false +end + +return can_handle_aeotec_minimote diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua new file mode 100644 index 0000000000..b63e217394 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0001, model = 0x0003} -- Aeotec Mimimote +} + +return ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua index 814bcb775b..3b8a04d90c 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,18 +10,7 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) -local ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0001, model = 0x0003} -- Aeotec Mimimote -} -local function can_handle_aeotec_minimote(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function basic_set_handler(self, device, cmd) local button = cmd.args.value // 40 + 1 @@ -59,7 +38,8 @@ local aeotec_minimote = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aeotec_minimote, + can_handle = require("zwave-multi-button.aeotec-minimote.can_handle"), + shared_device_thread_enabled = true, } return aeotec_minimote diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/can_handle.lua new file mode 100644 index 0000000000..a9d7d24be2 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zwave_multi_button(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button") + end + end + return false +end + +return can_handle_zwave_multi_button diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua new file mode 100644 index 0000000000..055e4f1eff --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_keyfob(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.fibaro-keyfob.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.fibaro-keyfob") + end + end + return false +end + +return can_handle_fibaro_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua new file mode 100644 index 0000000000..269d136689 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_FIBARO_KEYFOB_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU + {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US + {mfr = 0x010F, prod = 0x1001, model = 0x3000} -- Fibaro KeyFob AU +} + +return ZWAVE_FIBARO_KEYFOB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua index 653cd21ddd..178d440783 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua @@ -1,34 +1,11 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 4 }) -local ZWAVE_FIBARO_KEYFOB_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU - {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US - {mfr = 0x010F, prod = 0x1001, model = 0x3000} -- Fibaro KeyFob AU -} -local function can_handle_fibaro_keyfob(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_FIBARO_KEYFOB_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function do_configure(self, device) device:refresh() @@ -46,7 +23,8 @@ local fibaro_keyfob = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_fibaro_keyfob, + can_handle = require("zwave-multi-button.fibaro-keyfob.can_handle"), + shared_device_thread_enabled = true, } return fibaro_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua new file mode 100644 index 0000000000..4e539c90ab --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_MULTI_BUTTON_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU + {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US + {mfr = 0x010F, prod = 0x1001, model = 0x3000}, -- Fibaro KeyFob AU + {mfr = 0x0371, prod = 0x0002, model = 0x0003}, -- Aeotec NanoMote Quad EU + {mfr = 0x0371, prod = 0x0102, model = 0x0003}, -- Aeotec NanoMote Quad US + {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU + {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US + {mfr = 0x0086, prod = 0x0002, model = 0x0082}, -- Aeotec Wallmote Quad EU + {mfr = 0x0086, prod = 0x0102, model = 0x0082}, -- Aeotec Wallmote Quad US + {mfr = 0x0086, prod = 0x0002, model = 0x0081}, -- Aeotec Wallmote EU + {mfr = 0x0086, prod = 0x0102, model = 0x0081}, -- Aeotec Wallmote US + {mfr = 0x0060, prod = 0x000A, model = 0x0003}, -- Everspring Remote Control + {mfr = 0x0086, prod = 0x0001, model = 0x0003}, -- Aeotec Mimimote, + {mfr = 0x0371, prod = 0x0102, model = 0x0016}, -- Aeotec illumino Wallmote 7, + {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4, + {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4DC, +} + +return ZWAVE_MULTI_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua index 9094dc4111..57b9d42be4 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,33 +11,7 @@ local CentralScene = (require "st.zwave.CommandClass.CentralScene")({ version=1 --- @type st.zwave.CommandClass.SceneActivation local SceneActivation = (require "st.zwave.CommandClass.SceneActivation")({ version=1 }) -local ZWAVE_MULTI_BUTTON_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU - {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US - {mfr = 0x010F, prod = 0x1001, model = 0x3000}, -- Fibaro KeyFob AU - {mfr = 0x0371, prod = 0x0002, model = 0x0003}, -- Aeotec NanoMote Quad EU - {mfr = 0x0371, prod = 0x0102, model = 0x0003}, -- Aeotec NanoMote Quad US - {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU - {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US - {mfr = 0x0086, prod = 0x0002, model = 0x0082}, -- Aeotec Wallmote Quad EU - {mfr = 0x0086, prod = 0x0102, model = 0x0082}, -- Aeotec Wallmote Quad US - {mfr = 0x0086, prod = 0x0002, model = 0x0081}, -- Aeotec Wallmote EU - {mfr = 0x0086, prod = 0x0102, model = 0x0081}, -- Aeotec Wallmote US - {mfr = 0x0060, prod = 0x000A, model = 0x0003}, -- Everspring Remote Control - {mfr = 0x0086, prod = 0x0001, model = 0x0003}, -- Aeotec Mimimote, - {mfr = 0x0371, prod = 0x0102, model = 0x0016}, -- Aeotec illumino Wallmote 7, - {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4, - {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4DC, -} -local function can_handle_zwave_multi_button(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_MULTI_BUTTON_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local map_key_attribute_to_capability = { [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, @@ -115,12 +80,9 @@ local zwave_multi_button = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_zwave_multi_button, - sub_drivers = { - require("zwave-multi-button/aeotec-keyfob"), - require("zwave-multi-button/fibaro-keyfob"), - require("zwave-multi-button/aeotec-minimote") - } + can_handle = require("zwave-multi-button.can_handle"), + sub_drivers = require("zwave-multi-button.sub_drivers"), + shared_device_thread_enabled = true, } return zwave_multi_button diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua new file mode 100644 index 0000000000..6f532ab2af --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_shelly_wave_i4(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.shelly_wave_i4.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.shelly_wave_i4") + end + end + return false +end + +return can_handle_shelly_wave_i4 diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua new file mode 100644 index 0000000000..459a811d9a --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SHELLY_WAVE_i4_FINGERPRINTS = { + {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4 + {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4 DC +} + +return SHELLY_WAVE_i4_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua index 4e962cebb1..4b78cffac1 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua @@ -1,35 +1,13 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) -- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({ version=2 }) -local SHELLY_WAVE_i4_FINGERPRINTS = { - {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4 - {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4 DC -} -local function can_handle_shelly_wave_i4(opts, driver, device, ...) - for _, fingerprint in ipairs(SHELLY_WAVE_i4_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local do_configure = function(self, device) device:refresh() @@ -45,7 +23,8 @@ local shelly_wave_i4 = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_shelly_wave_i4, + can_handle = require("zwave-multi-button.shelly_wave_i4.can_handle"), + shared_device_thread_enabled = true, } return shelly_wave_i4 diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/sub_drivers.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/sub_drivers.lua new file mode 100644 index 0000000000..7ec5622dea --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-multi-button/aeotec-keyfob"), + lazy_load_if_possible("zwave-multi-button/fibaro-keyfob"), + lazy_load_if_possible("zwave-multi-button/aeotec-minimote"), + lazy_load_if_possible("zwave-multi-button/shelly_wave_i4"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-electric-meter/src/init.lua b/drivers/SmartThings/zwave-electric-meter/src/init.lua index 851de3096f..53c91cfa90 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/init.lua @@ -43,6 +43,7 @@ local driver_template = { added = device_added }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-fan/src/init.lua b/drivers/SmartThings/zwave-fan/src/init.lua index acdb34ae76..d3b51d8344 100644 --- a/drivers/SmartThings/zwave-fan/src/init.lua +++ b/drivers/SmartThings/zwave-fan/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -27,10 +17,8 @@ local driver_template = { capabilities.switch, capabilities.fanSpeed, }, - sub_drivers = { - require("zwave-fan-3-speed"), - require("zwave-fan-4-speed") - }, + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-fan/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-fan/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-fan/src/sub_drivers.lua b/drivers/SmartThings/zwave-fan/src/sub_drivers.lua new file mode 100644 index 0000000000..373e4daf52 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-fan-3-speed"), + lazy_load_if_possible("zwave-fan-4-speed"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua index 01c5a2b856..534c48698e 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua index 4c9b252877..80ccef948b 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/can_handle.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/can_handle.lua new file mode 100644 index 0000000000..66a04b41a9 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_fan_3_speed(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-fan-3-speed.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-fan-3-speed") + end + end + return false +end + +return is_fan_3_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua new file mode 100644 index 0000000000..7241769dcd --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FAN_3_SPEED_FINGERPRINTS = { + {mfr = 0x001D, prod = 0x1001, model = 0x0334}, -- Leviton 3-Speed Fan Controller + {mfr = 0x0063, prod = 0x4944, model = 0x3034}, -- GE In-Wall Smart Fan Control + {mfr = 0x0063, prod = 0x4944, model = 0x3131}, -- GE In-Wall Smart Fan Control + {mfr = 0x0039, prod = 0x4944, model = 0x3131}, -- Honeywell Z-Wave Plus In-Wall Fan Speed Control + {mfr = 0x0063, prod = 0x4944, model = 0x3337}, -- GE In-Wall Smart Fan Control +} + +return FAN_3_SPEED_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua index 282f2e35db..ad629ca54c 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local log = require "log" local capabilities = require "st.capabilities" @@ -22,13 +12,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local fan_speed_helper = (require "zwave_fan_helpers") -local FAN_3_SPEED_FINGERPRINTS = { - {mfr = 0x001D, prod = 0x1001, model = 0x0334}, -- Leviton 3-Speed Fan Controller - {mfr = 0x0063, prod = 0x4944, model = 0x3034}, -- GE In-Wall Smart Fan Control - {mfr = 0x0063, prod = 0x4944, model = 0x3131}, -- GE In-Wall Smart Fan Control - {mfr = 0x0039, prod = 0x4944, model = 0x3131}, -- Honeywell Z-Wave Plus In-Wall Fan Speed Control - {mfr = 0x0063, prod = 0x4944, model = 0x3337}, -- GE In-Wall Smart Fan Control -} local function map_fan_3_speed_to_switch_level (speed) if speed == fan_speed_helper.fan_speed.OFF then @@ -63,14 +46,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is an 3-speed fan, else false -local function is_fan_3_speed(opts, driver, device, ...) - for _, fingerprint in ipairs(FAN_3_SPEED_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -110,7 +85,8 @@ local zwave_fan_3_speed = { } }, NAME = "Z-Wave fan 3 speed", - can_handle = is_fan_3_speed, + can_handle = require("zwave-fan-3-speed.can_handle"), + shared_device_thread_enabled = true, } return zwave_fan_3_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua new file mode 100644 index 0000000000..b67f3c2ec7 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fan_4_speed(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-fan-4-speed.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-fan-4-speed") + end + end + return false +end + +return can_handle_fan_4_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua new file mode 100644 index 0000000000..2e2f2b13fb --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FAN_4_SPEED_FINGERPRINTS = { + {mfr = 0x001D, prod = 0x0038, model = 0x0002}, -- Leviton 4-Speed Fan Controller +} + +return FAN_4_SPEED_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua index f714a56b1b..62ec23bfa0 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local log = require "log" local capabilities = require "st.capabilities" @@ -22,9 +12,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local fan_speed_helper = (require "zwave_fan_helpers") -local FAN_4_SPEED_FINGERPRINTS = { - {mfr = 0x001D, prod = 0x0038, model = 0x0002}, -- Leviton 4-Speed Fan Controller -} local function map_fan_4_speed_to_switch_level (speed) if speed == fan_speed_helper.fan_speed.OFF then @@ -64,14 +51,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is 4-speed fan, else false -local function can_handle_fan_4_speed(opts, driver, device, ...) - for _, fingerprint in ipairs(FAN_4_SPEED_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -111,7 +90,8 @@ local zwave_fan_4_speed = { } }, NAME = "Z-Wave fan 4 speed", - can_handle = can_handle_fan_4_speed, + can_handle = require("zwave-fan-4-speed.can_handle"), + shared_device_thread_enabled = true, } return zwave_fan_4_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua b/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua index bb056cf06a..38c26fa78c 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.SwitchMultilevel diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua new file mode 100644 index 0000000000..571ba6bce1 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_garage_door(opts, driver, device, ...) + local FINGERPRINTS = require("ecolink-zw-gdo.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("ecolink-zw-gdo") + end + end + return false +end + +return can_handle_ecolink_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua new file mode 100644 index 0000000000..15de27ad0b --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Ecolink garage door operator +local ECOLINK_GARAGE_DOOR_FINGERPRINTS = { + {manufacturerId = 0x014A, productType = 0x0007, productId = 0x4731}, +} + +return ECOLINK_GARAGE_DOOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua index e6638841be..1019056330 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.capabilities local capabilities = require "st.capabilities" @@ -28,11 +17,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 8 }) --- Ecolink garage door operator -local ECOLINK_GARAGE_DOOR_FINGERPRINTS = { - manufacturerId = 0x014A, productType = 0x0007, productId = 0x4731 -} - local GDO_ENDPOINT_NAME = "main" local CONTACTSENSOR_ENDPOINT_NAME = "sensor" local GDO_ENDPOINT_NUMBER = 1 @@ -55,11 +39,6 @@ local GDO_CONFIG_PARAMS = { --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_ecolink_garage_door(opts, driver, device, ...) - return device:id_match(ECOLINK_GARAGE_DOOR_FINGERPRINTS.manufacturerId, - ECOLINK_GARAGE_DOOR_FINGERPRINTS.productType, - ECOLINK_GARAGE_DOOR_FINGERPRINTS.productId) -end local function component_to_endpoint(device, component_id) if (CONTACTSENSOR_ENDPOINT_NAME == component_id) then @@ -282,7 +261,8 @@ local ecolink_garage_door_operator = { doConfigure = configure_device_with_updated_config, infoChanged = configure_device_with_updated_config }, - can_handle = can_handle_ecolink_garage_door + can_handle = require("ecolink-zw-gdo.can_handle"), + shared_device_thread_enabled = true, } return ecolink_garage_door_operator diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/init.lua index 6d0a0a880c..3286471d83 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.Driver @@ -24,10 +14,8 @@ local driver_template = { capabilities.doorControl, capabilities.contactSensor, }, - sub_drivers = { - require("mimolite-garage-door"), - require("ecolink-zw-gdo") - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-garage-door-opener/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/can_handle.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/can_handle.lua new file mode 100644 index 0000000000..e515aae646 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_mimolite_garage_door(opts, driver, device, ...) + local FINGERPRINTS = require("mimolite-garage-door.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("mimolite-garage-door") + end + end + return false +end + +return can_handle_mimolite_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua new file mode 100644 index 0000000000..52f0969e84 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MIMOLITE_GARAGE_DOOR_FINGERPRINTS = { + { manufacturerId = 0x0084, productType = 0x0453, productId = 0x0111 } -- mimolite garage door +} + +return MIMOLITE_GARAGE_DOOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua index 1fd1b4362b..138a4f1b1d 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -28,23 +18,12 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local MIMOLITE_GARAGE_DOOR_FINGERPRINTS = { - { manufacturerId = 0x0084, productType = 0x0453, productId = 0x0111 } -- mimolite garage door -} --- Determine whether the passed device is mimolite garage door --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_mimolite_garage_door(opts, driver, device, ...) - for _, fingerprint in ipairs(MIMOLITE_GARAGE_DOOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function door_event_helper(device, value) device:emit_event(value == 0x00 and capabilities.doorControl.door.closed() or capabilities.doorControl.door.open()) @@ -118,7 +97,8 @@ local mimolite_garage_door = { doConfigure = do_configure }, NAME = "mimolite garage door", - can_handle = can_handle_mimolite_garage_door + can_handle = require("mimolite-garage-door.can_handle"), + shared_device_thread_enabled = true, } return mimolite_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/sub_drivers.lua b/drivers/SmartThings/zwave-garage-door-opener/src/sub_drivers.lua new file mode 100644 index 0000000000..8de6edfd56 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("mimolite-garage-door"), + lazy_load_if_possible("ecolink-zw-gdo"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua index 8d0ebfb7a9..0608ca2ef2 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua index 319a823daf..cc33391a4c 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua index 1fd7c8a3d6..0d5da468e8 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua deleted file mode 100644 index 0204b7b2d5..0000000000 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index b83b196256..c3506c5005 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -182,13 +171,8 @@ local driver_template = { [Time.GET] = time_get_handler -- used by DanaLock } }, - sub_drivers = { - require("zwave-alarm-v1-lock"), - require("schlage-lock"), - require("samsung-lock"), - require("keywe-lock"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..d8bcd5756e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + local KEYWE_MFR = 0x037B + if device.zwave_manufacturer_id == KEYWE_MFR then + return true, require("keywe-lock") + end + return false +end + +return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index d39aa45d1c..a51af26e00 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -23,13 +13,8 @@ local LockDefaults = require "st.zwave.defaults.lock" local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local TamperDefaults = require "st.zwave.defaults.tamperAlert" -local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - local function clear_tamper_if_needed(device) local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) if current_tamper_state == "detected" then @@ -80,7 +65,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = can_handle_keywe_lock, + can_handle = require("keywe-lock.can_handle"), } return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..e9222cb8fb --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + return true, require("samsung-lock") + end + return false +end + +return can_handle_samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua index 813c6217b4..b2f4f60975 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -28,11 +18,6 @@ local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted -local SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -end local function get_ongoing_code_set(device) local code_id @@ -105,7 +90,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = can_handle_samsung_lock, + can_handle = require("samsung-lock.can_handle"), } return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..e9f3cfb84c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + return true, require("schlage-lock") + end + return false +end + +return can_handle_schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua index 67e649d869..6b22049beb 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -27,15 +17,10 @@ local Association = (require "st.zwave.CommandClass.Association")({version=1}) local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - local function set_code_length(self, device, cmd) local length = cmd.args.length if length >= 4 and length <= 8 then @@ -187,7 +172,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = can_handle_schlage_lock, + can_handle = require("schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..627fe99514 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-alarm-v1-lock"), + lazy_load_if_possible("schlage-lock"), + lazy_load_if_possible("samsung-lock"), + lazy_load_if_possible("keywe-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index 0ca48029b5..09d59f4861 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index fed7dceabd..11c03650c6 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 31bd0241b6..81ba1df2ad 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index c5bb158c23..38dabbd9dd 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index a68f12789e..c798feca08 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua index fa094b8cfa..1eb2d093e5 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..7bb54f23f2 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + return true, require("zwave-alarm-v1-lock") + end + return false +end + +return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua index 44d978999b..d7c862f22a 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -35,9 +25,6 @@ local METHOD = { --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end --- Default handler for alarm command class reports, these were largely OEM-defined --- @@ -159,7 +146,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-alarm-v1-lock.can_handle"), } return zwave_lock diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua new file mode 100644 index 0000000000..d9956517dd --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_multisensor(opts, self, device, ...) + local FINGERPRINTS = require("aeotec-multisensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-multisensor") + return true, subdriver + end + end + return false +end + +return can_handle_aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua new file mode 100644 index 0000000000..9436a85979 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_MULTISENSOR_FINGERPRINTS = { + { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 + { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 +} + +return AEOTEC_MULTISENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua index edd01c7553..947f2fea2b 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,21 +7,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local AEOTEC_MULTISENSOR_FINGERPRINTS = { - { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 - { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 -} - -local function can_handle_aeotec_multisensor(opts, self, device, ...) - for _, fingerprint in ipairs(AEOTEC_MULTISENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-multisensor") - return true, subdriver - end - end - return false -end - local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -61,12 +35,10 @@ local aeotec_multisensor = { [Notification.REPORT] = notification_report_handler } }, - sub_drivers = { - require("aeotec-multisensor/multisensor-6"), - require("aeotec-multisensor/multisensor-7") - }, + sub_drivers = require("aeotec-multisensor.sub_drivers"), NAME = "aeotec multisensor", - can_handle = can_handle_aeotec_multisensor + can_handle = require("aeotec-multisensor.can_handle"), + shared_device_thread_enabled = true, } return aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua new file mode 100644 index 0000000000..d86e9c8b3a --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multisensor_6(opts, self, device, ...) +local MULTISENSOR_6_PRODUCT_ID = 0x0064 + if device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID then + return true, require("aeotec-multisensor.multisensor-6") + end + return false +end +return can_handle_multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua index 1b9d4d6b97..f8d001cdf4 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -19,12 +10,8 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version = 2}) -local MULTISENSOR_6_PRODUCT_ID = 0x0064 local PREFERENCE_NUM = 9 -local function can_handle_multisensor_6(opts, self, device, ...) - return device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID -end local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -62,7 +49,8 @@ local multisensor_6 = { } }, NAME = "aeotec multisensor 6", - can_handle = can_handle_multisensor_6 + can_handle = require("aeotec-multisensor.multisensor-6.can_handle"), + shared_device_thread_enabled = true, } return multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua new file mode 100644 index 0000000000..f109d0e31c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multisensor_7(opts, self, device, ...) + local MULTISENSOR_7_PRODUCT_ID = 0x0018 + if device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID then + return true, require("aeotec-multisensor.multisensor-7") + end + return false +end + +return can_handle_multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua index 2d2bf4e36e..d97d65759c 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -19,13 +10,8 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 2 }) -local MULTISENSOR_7_PRODUCT_ID = 0x0018 local PREFERENCE_NUM = 10 -local function can_handle_multisensor_7(opts, self, device, ...) - return device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID -end - local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -62,7 +48,8 @@ local multisensor_7 = { } }, NAME = "aeotec multisensor 7", - can_handle = can_handle_multisensor_7 + can_handle = require("aeotec-multisensor.multisensor-7.can_handle"), + shared_device_thread_enabled = true, } return multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua new file mode 100644 index 0000000000..396f53fe86 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aeotec-multisensor/multisensor-6"), + lazy_load_if_possible("aeotec-multisensor/multisensor-7"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua new file mode 100644 index 0000000000..1b87febb74 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-water-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-water-sensor") + return true, subdriver + end + end + return false +end + +return can_handle_zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua new file mode 100644 index 0000000000..423d87754e --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU + { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US + { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU + { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 +} + +return ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua index 9d883ea3c2..509e38ac9e 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +9,8 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { - { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU - { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US - { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU - { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 -} --- Determine whether the passed device is zwave water temperature humidiry sensor -local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-water-sensor") - return true, subdriver - end - end - return false -end --- Default handler for notification command class reports --- @@ -68,7 +44,8 @@ local zwave_water_temp_humidity_sensor = { }, }, NAME = "zwave water temp humidity sensor", - can_handle = can_handle_zwave_water_temp_humidity_sensor + can_handle = require("aeotec-water-sensor.can_handle"), + shared_device_thread_enabled = true, } return zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua deleted file mode 100644 index 322333d565..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,46 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - --- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll -local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor -local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat -local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 -local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 -local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor -local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 -local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} -local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, - AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, - ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - - for _, fp in ipairs(FPS) do - if device:id_match(fp.mfr, fp.prod, fp.model) then return false end - end - local subdriver = require("apiv6_bugfix") - return true, subdriver - else - return false - end -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-sensor/src/configurations.lua b/drivers/SmartThings/zwave-sensor/src/configurations.lua index 2883e70384..0a3c62ead8 100644 --- a/drivers/SmartThings/zwave-sensor/src/configurations.lua +++ b/drivers/SmartThings/zwave-sensor/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..8ab0bac6bc --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) + local ENERWAVE_MFR = 0x011A + if device.zwave_manufacturer_id == ENERWAVE_MFR then + local subdriver = require("enerwave-motion-sensor") + return true, subdriver + else return false end +end + +return can_handle_enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua index 6fb712e3b0..53ef77c4be 100644 --- a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,15 +10,6 @@ local Association = (require "st.zwave.CommandClass.Association")({version=2}) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) -local ENERWAVE_MFR = 0x011A - -local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) - if device.zwave_manufacturer_id == ENERWAVE_MFR then - local subdriver = require("enerwave-motion-sensor") - return true, subdriver - else return false end -end - local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -58,7 +39,8 @@ local enerwave_motion_sensor = { doConfigure = do_configure }, NAME = "enerwave_motion_sensor", - can_handle = can_handle_enerwave_motion_sensor + can_handle = require("enerwave-motion-sensor.can_handle"), + shared_device_thread_enabled = true, } return enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua new file mode 100644 index 0000000000..c9fd2eafd1 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_everspring_motion_light(opts, driver, device, ...) + local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } + if device:id_match( + EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.model + ) then + local subdriver = require("everspring-motion-light-sensor") + return true, subdriver + end + return false +end + +return can_handle_everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua index 1b11aadabe..8baa3756d6 100644 --- a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua @@ -1,34 +1,12 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2,strict=true}) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version=2}) -local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } - -local function can_handle_everspring_motion_light(opts, driver, device, ...) - if device:id_match( - EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.model - ) then - local subdriver = require("everspring-motion-light-sensor") - return true, subdriver - else return false end -end - local function device_added(driver, device) device:emit_event(capabilities.motionSensor.motion.inactive()) device:send(SwitchBinary:Get({})) @@ -40,7 +18,7 @@ local everspring_motion_light = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_everspring_motion_light + can_handle = require("everspring-motion-light-sensor.can_handle"), } return everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua new file mode 100644 index 0000000000..5596894fbb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) + local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } + if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then + local subdriver = require("ezmultipli-multipurpose-sensor") + return true, subdriver + end + return false +end + +return can_handle_ezmultipli_multipurpose_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua index 1e4b3bf0ce..64f34ffd96 100644 --- a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.utils @@ -28,17 +19,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) local CAP_CACHE_KEY = "st.capabilities." .. capabilities.colorControl.ID -local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } - -local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) - if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("ezmultipli-multipurpose-sensor") - return true, subdriver - else return false end -end - local function basic_report_handler(driver, device, cmd) local event local value = (cmd.args.target_value ~= nil) and cmd.args.target_value or cmd.args.value @@ -102,7 +82,8 @@ local ezmultipli_multipurpose_sensor = { [capabilities.colorControl.commands.setColor.NAME] = set_color } }, - can_handle = can_handle_ezmultipli_multipurpose_sensor + can_handle = require("ezmultipli-multipurpose-sensor.can_handle"), + shared_device_thread_enabled = true, } -return ezmultipli_multipurpose_sensor \ No newline at end of file +return ezmultipli_multipurpose_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua new file mode 100644 index 0000000000..6cf9ebba98 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then + local subdriver = require("fibaro-door-window-sensor") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua new file mode 100644 index 0000000000..992ea8fd9d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-1") + end + end + return false +end + +return can_handle_fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua new file mode 100644 index 0000000000..50727133bb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } +} + +return FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua index 698fffcceb..abe2a8b6dc 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -21,19 +10,6 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 1 }) local configurationsMap = require "configurations" -local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -} - -local function can_handle_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end - local function sensor_alarm_report_handler(driver, device, cmd) if (cmd.args.sensor_state == SensorAlarm.sensor_state.ALARM) then device:emit_event(capabilities.tamperAlert.tamper.detected()) @@ -92,7 +68,8 @@ local fibaro_door_window_sensor_1 = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh }, - can_handle = can_handle_fibaro_door_window_sensor_1 + can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.can_handle"), + shared_device_thread_enabled = true, } return fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua new file mode 100644 index 0000000000..4493496f94 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-2") + end + end + return false +end + +return can_handle_fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua new file mode 100644 index 0000000000..6103c107d1 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ +} + +return FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua index 16c5ec2017..a1f8ccee82 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,21 +7,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) -local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ -} - -local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end - local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) if device:get_latest_state(component, capability.ID, attribute_name) == nil then device:emit_event(value) @@ -83,7 +57,8 @@ local fibaro_door_window_sensor_2 = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_fibaro_door_window_sensor_2, + can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.can_handle"), + shared_device_thread_enabled = true, } return fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua new file mode 100644 index 0000000000..699df3f623 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor +} + +return FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua index 86cf865348..dc26fcddcd 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -24,27 +13,6 @@ local preferencesMap = require "preferences" local FIBARO_DOOR_WINDOW_SENSOR_WAKEUP_INTERVAL = 21600 --seconds -local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor -} - -local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then - local subdriver = require("fibaro-door-window-sensor") - return true, subdriver - end - end - return false -end - local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do if parameter.parameter_number == parameterNumber then @@ -154,11 +122,9 @@ local fibaro_door_window_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - sub_drivers = { - require("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), - require("fibaro-door-window-sensor/fibaro-door-window-sensor-2") - }, - can_handle = can_handle_fibaro_door_window_sensor + sub_drivers = require("fibaro-door-window-sensor.sub_drivers"), + can_handle = require("fibaro-door-window-sensor.can_handle"), + shared_device_thread_enabled = true, } return fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua new file mode 100644 index 0000000000..0c4ddd4e43 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), + lazy_load_if_possible("fibaro-door-window-sensor/fibaro-door-window-sensor-2"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua new file mode 100644 index 0000000000..341fbcd6f9 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_flood_sensor(opts, driver, device, ...) + local FIBARO_MFR_ID = 0x010F + local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } + if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then + local subdriver = require("fibaro-flood-sensor") + return true, subdriver + end + return false +end + +return can_handle_fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index 144be985ae..75fc982511 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -26,17 +17,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = local preferences = require "preferences" local configurations = require "configurations" -local FIBARO_MFR_ID = 0x010F -local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } - -local function can_handle_fibaro_flood_sensor(opts, driver, device, ...) - if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then - local subdriver = require("fibaro-flood-sensor") - return true, subdriver - else return false end -end - - local function basic_set_handler(self, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -96,7 +76,8 @@ local fibaro_flood_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_fibaro_flood_sensor + can_handle = require("fibaro-flood-sensor.can_handle"), + shared_device_thread_enabled = true, } return fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..b184001935 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) + + local FIBARO_MOTION_MFR = 0x010F + local FIBARO_MOTION_PROD = 0x0800 + if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then + local subdriver = require("fibaro-motion-sensor") + return true, subdriver + end + return false +end + +return can_handle_fibaro_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua index ae45f5a27b..e1aa2ccaf8 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -18,15 +8,6 @@ local cc = require "st.zwave.CommandClass" local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 }) local capabilities = require "st.capabilities" -local FIBARO_MOTION_MFR = 0x010F -local FIBARO_MOTION_PROD = 0x0800 - -local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) - if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then - local subdriver = require("fibaro-motion-sensor") - return true, subdriver - else return false end -end local function sensor_alarm_report(driver, device, cmd) if (cmd.args.sensor_state ~= SensorAlarm.sensor_state.NO_ALARM) then @@ -43,7 +24,8 @@ local fibaro_motion_sensor = { [SensorAlarm.REPORT] = sensor_alarm_report } }, - can_handle = can_handle_fibaro_motion_sensor + can_handle = require("fibaro-motion-sensor.can_handle"), + shared_device_thread_enabled = true, } -return fibaro_motion_sensor \ No newline at end of file +return fibaro_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua new file mode 100644 index 0000000000..3ecdc2baf0 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +--This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled +local FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor +} + +return function(opts, driver, device, ...) + if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subDriver = require("firmware-version") + return true, subDriver + end + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua index 058a7f955c..8fe58a7167 100644 --- a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,22 +10,6 @@ local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) ---This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled -local FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor -} - -local function can_handle_fw(opts, driver, device, ...) - if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subDriver = require("firmware-version") - return true, subDriver - end - end - end - return false -end --Runs upstream handlers (ex zwave_handlers) local function call_parent_handler(handlers, self, device, event, args) @@ -73,7 +47,7 @@ end local firmware_version = { NAME = "firmware_version", - can_handle = can_handle_fw, + can_handle = require("firmware-version.can_handle"), lifecycle_handlers = { added = added_handler, @@ -85,7 +59,8 @@ local firmware_version = { [cc.WAKE_UP] = { [WakeUp.NOTIFICATION] = wakeup_notification } - } + }, + shared_device_thread_enabled = true, } -return firmware_version \ No newline at end of file +return firmware_version diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua new file mode 100644 index 0000000000..e24d7b9cf2 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is glentronics water leak sensor +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) + local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor + if device:id_match( + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then + return true, require("glentronics-water-leak-sensor") + end + return false +end + +return can_handle_glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua index 3dba7351d6..cc6c476658 100644 --- a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +9,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor - ---- Determine whether the passed device is glentronics water leak sensor ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) - if device:id_match( - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("glentronics-water-leak-sensor") - return true, subdriver - else return false end -end - local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -78,7 +52,8 @@ local glentronics_water_leak_sensor = { added = device_added }, NAME = "glentronics water leak sensor", - can_handle = can_handle_glentronics_water_leak_sensor + can_handle = require("glentronics-water-leak-sensor.can_handle"), + shared_device_thread_enabled = true, } return glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua new file mode 100644 index 0000000000..992c1f7c7f --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is homeseer multi sensor +--- +--- @param driver Driver driver instance +--- @param device Device device instance +--- @return boolean true if the device proper, else false +local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) + local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 + if device:id_match( + HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then + return true, require("homeseer-multi-sensor") + end + return false +end + +return can_handle_homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua index 2330f28106..07835ac7d4 100644 --- a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,23 +13,6 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version = 5}) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1}) -local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 - ---- Determine whether the passed device is homeseer multi sensor ---- ---- @param driver Driver driver instance ---- @param device Device device instance ---- @return boolean true if the device proper, else false -local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) - if device:id_match( - HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("homeseer-multi-sensor") - return true, subdriver - else return false end -end - local function basic_set_handler(self, device, cmd) if cmd.args.value ~= nil then device:emit_event(cmd.args.value == 0xFF and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -87,7 +61,8 @@ local homeseer_multi_sensor = { init = device_init, }, NAME = "homeseer multi sensor", - can_handle = can_handle_homeseer_multi_sensor + can_handle = require("homeseer-multi-sensor.can_handle"), + shared_device_thread_enabled = true, } return homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 213aa8c389..9ab4d0077d 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -27,19 +16,6 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local preferences = require "preferences" local configurations = require "configurations" -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - --- Handle preference changes --- --- @param driver st.zwave.Driver @@ -134,27 +110,7 @@ local driver_template = { capabilities.powerMeter, capabilities.smokeDetector }, - sub_drivers = { - lazy_load_if_possible("zooz-4-in-1-sensor"), - lazy_load_if_possible("vision-motion-detector"), - lazy_load_if_possible("fibaro-flood-sensor"), - lazy_load_if_possible("aeotec-water-sensor"), - lazy_load_if_possible("glentronics-water-leak-sensor"), - lazy_load_if_possible("homeseer-multi-sensor"), - lazy_load_if_possible("fibaro-door-window-sensor"), - lazy_load_if_possible("sensative-strip"), - lazy_load_if_possible("enerwave-motion-sensor"), - lazy_load_if_possible("aeotec-multisensor"), - lazy_load_if_possible("zwave-water-leak-sensor"), - lazy_load_if_possible("everspring-motion-light-sensor"), - lazy_load_if_possible("ezmultipli-multipurpose-sensor"), - lazy_load_if_possible("fibaro-motion-sensor"), - lazy_load_if_possible("v1-contact-event"), - lazy_load_if_possible("timed-tamper-clear"), - lazy_load_if_possible("wakeup-no-poll"), - lazy_load_if_possible("firmware-version"), - lazy_load_if_possible("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { added = added_handler, init = device_init, @@ -169,6 +125,7 @@ local driver_template = { [WakeUp.NOTIFICATION] = wakeup_notification } }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, diff --git a/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 9585b6ffe9..70293b10fa 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua new file mode 100644 index 0000000000..9b515bae9f --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_sensative_strip(opts, driver, device, cmd, ...) + local SENSATIVE_MFR = 0x019A + local SENSATIVE_MODEL = 0x000A + if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then + local subdriver = require("sensative-strip") + return true, subdriver + end + return false +end + +return can_handle_sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua index 73f1cb8459..89085c00c9 100644 --- a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -19,20 +9,11 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local SENSATIVE_MFR = 0x019A -local SENSATIVE_MODEL = 0x000A local LEAKAGE_ALARM_PARAM = 12 local LEAKAGE_ALARM_OFF = 0 local SENSATIVE_COMFORT_PROFILE = "illuminance-temperature" local CONFIG_REPORT_RECEIVED = "configReportReceived" -local function can_handle_sensative_strip(opts, driver, device, cmd, ...) - if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then - local subdriver = require("sensative-strip") - return true, subdriver - else return false end -end - local function configuration_report(driver, device, cmd) local parameter_number = cmd.args.parameter_number local configuration_value = cmd.args.configuration_value @@ -75,7 +56,8 @@ local sensative_strip = { doConfigure = do_configure }, NAME = "sensative_strip", - can_handle = can_handle_sensative_strip + can_handle = require("sensative-strip.can_handle"), + shared_device_thread_enabled = true, } return sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..9500731597 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua @@ -0,0 +1,25 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("zooz-4-in-1-sensor"), + lazy_load_if_possible("vision-motion-detector"), + lazy_load_if_possible("fibaro-flood-sensor"), + lazy_load_if_possible("aeotec-water-sensor"), + lazy_load_if_possible("glentronics-water-leak-sensor"), + lazy_load_if_possible("homeseer-multi-sensor"), + lazy_load_if_possible("fibaro-door-window-sensor"), + lazy_load_if_possible("sensative-strip"), + lazy_load_if_possible("enerwave-motion-sensor"), + lazy_load_if_possible("aeotec-multisensor"), + lazy_load_if_possible("zwave-water-leak-sensor"), + lazy_load_if_possible("everspring-motion-light-sensor"), + lazy_load_if_possible("ezmultipli-multipurpose-sensor"), + lazy_load_if_possible("fibaro-motion-sensor"), + lazy_load_if_possible("v1-contact-event"), + lazy_load_if_possible("timed-tamper-clear"), + lazy_load_if_possible("wakeup-no-poll"), + lazy_load_if_possible("firmware-version"), +} diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua index 934235ae24..472932fdfb 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua index 29938eb5ce..323c0d4807 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua index ed0e6312b5..13442ec635 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua index 63671f073b..b6ede32bd5 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua index 836b5a9480..e38ce96f37 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua index bdb0f60308..0f3dc972de 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua index a1e9f5691e..3559656e3a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua index 853729bdd5..a33f9b97ec 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua index 428a4883b0..8479a5f74f 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua index 0e537bf123..3c288c0be4 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua index 48c3d2e609..ab124e8f80 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua index 5f9adaf70c..0a31f97de5 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua index 6e5f1d25ed..fb2138c0d5 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua index 1b4ba1cd9c..05e306b6c7 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua index 16f46f0756..6707dd7ae0 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua index c784afb875..8fd23b208b 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua index cd986cdc94..4d3e6e0660 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua index 51533e76da..0ac7e57418 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua index d9fd3c08c3..530d1c03bb 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua index 53ce347428..3dbfd89bc7 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua index a4931e34f3..ac2022e069 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua index 1315ffe638..3496ebb6d5 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua index b78bc8df64..926558036b 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua index 2549508083..743ffc5298 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua b/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua index 0ee1fe63e6..c88527f0cd 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua @@ -1,16 +1,6 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -115,4 +105,3 @@ test.register_message_test( ) test.run_registered_tests() - diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua index 72e0e59d07..fb9b519a42 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua index 7fdd26954c..e80e28dbd9 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua b/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua index 422347f770..4cfd906636 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua @@ -1,16 +1,6 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua b/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua index 1b372a9162..3cd16b6fa3 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua index 9737b0d863..fde2d5ebfe 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua index df28af97d1..7a8adbeb10 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua index 56549ac78e..8e463e7625 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua index 7f234a05fb..677204e5d1 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua index ab296f8fed..775130d87e 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua new file mode 100644 index 0000000000..c05cbdcf7d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_tamper_event(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) + local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F + + if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and + opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or + cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then + return true, require("timed-tamper-clear") + end + return false +end + +return can_handle_tamper_event diff --git a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua index 2007bedb0d..8554dab28f 100644 --- a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua @@ -1,16 +1,7 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -20,23 +11,6 @@ local capabilities = require "st.capabilities" local TAMPER_TIMER = "_tamper_timer" local TAMPER_CLEAR = 10 -local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F - -local function can_handle_tamper_event(opts, driver, device, cmd, ...) - if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and - opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or - cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then - local subdriver = require("timed-tamper-clear") - return true, subdriver - else return false - end -end -- This behavior is from zwave-door-window-sensor.groovy. We've seen this behavior -- in Ecolink and several other z-wave sensors that do not send tamper clear events @@ -60,7 +34,8 @@ local timed_tamper_clear = { } }, NAME = "timed tamper clear", - can_handle = can_handle_tamper_event + can_handle = require("timed-tamper-clear.can_handle"), + shared_device_thread_enabled = true, } return timed_tamper_clear diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua new file mode 100644 index 0000000000..492a72dc74 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua @@ -0,0 +1,22 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) + + if opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + cmd.args.v1_alarm_type == 0x07 then + local subdriver = require("v1-contact-event") + return true, subdriver + else + return false + end +end + +return can_handle_v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua index 40efc7633e..78b3beefcb 100644 --- a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -18,20 +7,6 @@ local cc = require "st.zwave.CommandClass" local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) local capabilities = require "st.capabilities" -local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) - if opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - cmd.args.v1_alarm_type == 0x07 then - local subdriver = require("v1-contact-event") - return true, subdriver - else - return false - end -end -- This behavior is from zwave-door-window-sensor.groovy, where it is -- indicated that certain monoprice sensors had this behavior. Also, @@ -53,7 +28,8 @@ local v1_contact_event = { } }, NAME = "v1 contact event", - can_handle = can_handle_v1_contact_event + can_handle = require("v1-contact-event.can_handle"), + shared_device_thread_enabled = true, } return v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua new file mode 100644 index 0000000000..d270a7954d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is zwave-plus-motion-temp-sensor +local function can_handle_vision_motion_detector(opts, driver, device, ...) + local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 + if device:id_match( + VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, + VISION_MOTION_DETECTOR_FINGERPRINTS.productType, + VISION_MOTION_DETECTOR_FINGERPRINTS.productId + ) then + return true, require("vision-motion-detector") + end + return false +end + +return can_handle_vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua index 320bf3824f..f9e8f8a04e 100644 --- a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,20 +13,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 - ---- Determine whether the passed device is zwave-plus-motion-temp-sensor -local function can_handle_vision_motion_detector(opts, driver, device, ...) - if device:id_match( - VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, - VISION_MOTION_DETECTOR_FINGERPRINTS.productType, - VISION_MOTION_DETECTOR_FINGERPRINTS.productId - ) then - local subdriver = require("vision-motion-detector") - return true, subdriver - else return false end -end - --- Handler for notification report command class from sensor --- --- @param self st.zwave.Driver @@ -83,7 +60,8 @@ local vision_motion_detector = { doConfigure = do_configure, }, NAME = "Vision motion detector", - can_handle = can_handle_vision_motion_detector + can_handle = require("vision-motion-detector.can_handle"), + shared_device_thread_enabled = true, } return vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua new file mode 100644 index 0000000000..15ac66d439 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("wakeup-no-poll") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua index 59d298a0e4..42163aeaae 100644 --- a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua @@ -1,16 +1,7 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -21,17 +12,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version = 2 --- @type st.zwave.CommandClass.Battery local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor - -local function can_handle(opts, driver, device, ...) - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("wakeup-no-poll") - return true, subdriver - else - return false - end -end - -- Nortek open/closed sensors _always_ respond with "open" when polled, and they are polled after wakeup local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -53,7 +33,8 @@ local wakeup_no_poll = { [WakeUp.NOTIFICATION] = wakeup_notification } }, - can_handle = can_handle + can_handle = require("wakeup-no-poll.can_handle"), + shared_device_thread_enabled = true, } -return wakeup_no_poll \ No newline at end of file +return wakeup_no_poll diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua new file mode 100644 index 0000000000..e979e32153 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zooz-4-in-1-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("zooz-4-in-1-sensor") + return true, subdriver + end + end + return false +end + +return can_handle_zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua new file mode 100644 index 0000000000..12d853b147 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZOOZ_4_IN_1_FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor + { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion + { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 +} + +return ZOOZ_4_IN_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua index 5d4570e525..2a4053302b 100644 --- a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,22 +13,8 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.utils local utils = require "st.utils" -local ZOOZ_4_IN_1_FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor - { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion - { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 -} --- Determine whether the passed device is zooz_4_in_1_sensor -local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_4_IN_1_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("zooz-4-in-1-sensor") - return true, subdriver - end - end - return false -end --- Handler for notification report command class --- @@ -109,7 +86,8 @@ local zooz_4_in_1_sensor = { } }, NAME = "zooz 4 in 1 sensor", - can_handle = can_handle_zooz_4_in_1_sensor + can_handle = require("zooz-4-in-1-sensor.can_handle"), + shared_device_thread_enabled = true, } return zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua new file mode 100644 index 0000000000..63da2ae39c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_water_leak_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-water-leak-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zwave-water-leak-sensor") + return true, subdriver + end + end + return false +end + +return can_handle_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua new file mode 100644 index 0000000000..c07bdb52cb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WATER_LEAK_SENSOR_FINGERPRINTS = { + {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor + {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor + {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor + {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) + {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) + {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) + {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor + {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector + {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor +} + +return WATER_LEAK_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua index 1eefab7479..6a956a4343 100644 --- a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua @@ -1,47 +1,10 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) - -local WATER_LEAK_SENSOR_FINGERPRINTS = { - {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor - {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor - {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor - {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) - {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) - {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) - {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor - {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector - {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor -} - -local function can_handle_water_leak_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(WATER_LEAK_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zwave-water-leak-sensor") - return true, subdriver - end - end - return false -end - local function basic_set_handler(driver, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -54,7 +17,8 @@ local water_leak_sensor = { [Basic.SET] = basic_set_handler } }, - can_handle = can_handle_water_leak_sensor + can_handle = require("zwave-water-leak-sensor.can_handle"), + shared_device_thread_enabled = true, } return water_leak_sensor diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 3f4b44c1e0..0000000000 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION - then - return true, require("apiv6_bugfix") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua deleted file mode 100644 index 2e7e3ca3b8..0000000000 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,23 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-siren/src/init.lua b/drivers/SmartThings/zwave-siren/src/init.lua index 52ccaba6b9..94e8b723b9 100644 --- a/drivers/SmartThings/zwave-siren/src/init.lua +++ b/drivers/SmartThings/zwave-siren/src/init.lua @@ -88,7 +88,8 @@ local driver_template = { infoChanged = info_changed, doConfigure = do_configure, added = added_handler - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua index 12ce423ba5..52d5035a16 100644 --- a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua @@ -14,6 +14,5 @@ local sub_drivers = { lazy_load_if_possible("zipato-siren"), lazy_load_if_possible("utilitech-siren"), lazy_load_if_possible("fortrezz"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua deleted file mode 100644 index 0204b7b2d5..0000000000 --- a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = can_handle -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua new file mode 100644 index 0000000000..4a3f51183b --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-smoke-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-smoke-sensor") + end + end + return false +end + +return can_handle_fibaro_smoke_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua new file mode 100644 index 0000000000..81c04eccfc --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor +} + +return FIBARO_SMOKE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua index a4d62fa1a4..bf7d554613 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,26 +14,12 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) local FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL = 21600 --seconds -local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor -} --- Determine whether the passed device is fibaro smoke sensro --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is fibaro smoke sensor -local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_SMOKE_SENSOR_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function device_added(self, device) device:send(WakeUp:IntervalSet({node_id = self.environment_info.hub_zwave_id, seconds = FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL})) @@ -76,7 +52,7 @@ local fibaro_smoke_sensor = { added = device_added }, NAME = "fibaro smoke sensor", - can_handle = can_handle_fibaro_smoke_sensor, + can_handle = require("fibaro-smoke-sensor.can_handle"), health_check = false, } diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua index 0d0a5d1bfd..1ee506f453 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -83,18 +73,14 @@ local driver_template = { capabilities.temperatureAlarm, capabilities.temperatureMeasurement }, - sub_drivers = { - require("zwave-smoke-co-alarm-v1"), - require("zwave-smoke-co-alarm-v2"), - require("fibaro-smoke-sensor"), - require("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, infoChanged = info_changed, doConfigure = do_configure, added = device_added - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua index 25606a5255..55fa70d48b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { FIBARO = { diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua new file mode 100644 index 0000000000..39901aaf21 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-smoke-co-alarm-v1"), + lazy_load_if_possible("zwave-smoke-co-alarm-v2"), + lazy_load_if_possible("fibaro-smoke-sensor"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua index 00259cbfe3..e860e4608f 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua index 0b1e240fa6..39c79e0498 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua index da12cddc06..bdeb10535f 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua index 655ef04d8d..60710942bc 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua index 841d6f6b34..c62dab7ec6 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua new file mode 100644 index 0000000000..dc78d0e4ac --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + -- The default handlers for the Alarm/Notification command class(es) for the + -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 then + return true, require("zwave-smoke-co-alarm-v1") + end + return false +end + +return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua index 923c516167..8e3cc4e267 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -23,17 +12,6 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0002 -- First Alert Smoke & CO Detector -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0003 -- First Alert Smoke & CO Detector ---- Determine whether the passed device only supports V1 or V2 of the Alarm command class ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - -- The default handlers for the Alarm/Notification command class(es) for the - -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 -end - --- Default handler for alarm command class reports --- --- This converts alarm V1 reports to correct smoke events @@ -75,7 +53,7 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-smoke-co-alarm-v1.can_handle"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua new file mode 100644 index 0000000000..6666e7abc2 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v2_alarm(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2") + end + end + return false +end + +return can_handle_v2_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua new file mode 100644 index 0000000000..baa0d7bbf0 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5") + end + end + return false +end + +return can_handle_fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua new file mode 100644 index 0000000000..37fb1f508c --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_CO_SENSORS_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return FIBARO_CO_SENSORS_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua index bde1cbc877..0559b83983 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) @@ -21,10 +11,6 @@ local TAMPERING_AND_EXCEEDING_THE_TEMPERATURE = 3 local ACOUSTIC_SIGNALS = 4 local EXCEEDING_THE_TEMPERATURE = 2 -local FIBARO_CO_SENSORS_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do @@ -40,14 +26,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_CO_SENSORS_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function update_preferences(self, device, args) local preferences = preferencesMap.get_device_parameters(device) @@ -108,7 +86,7 @@ local fibaro_co_sensor = { init = device_init, infoChanged = info_changed }, - can_handle = can_handle_fibaro_co_sensor + can_handle = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.can_handle"), } return fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua new file mode 100644 index 0000000000..8dc021cc28 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SMOKE_CO_ALARM_V2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return SMOKE_CO_ALARM_V2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua index b49b010cdb..5e69f7108d 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,24 +11,12 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local SMOKE_CO_ALARM_V2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} --- Determine whether the passed device is Smoke Alarm --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v2_alarm(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(SMOKE_CO_ALARM_V2_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local device_added = function(self, device) device:emit_event(capabilities.carbonMonoxideDetector.carbonMonoxide.clear()) @@ -94,13 +73,11 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V2", - can_handle = can_handle_v2_alarm, + can_handle = require("zwave-smoke-co-alarm-v2.can_handle"), lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5") - } + sub_drivers = require("zwave-smoke-co-alarm-v2.sub_drivers"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua new file mode 100644 index 0000000000..0699743371 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 9f40fd84e4..7d622e586f 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -125,7 +125,8 @@ local driver_template = { infoChanged = info_changed, doConfigure = do_configure, added = device_added - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 079eeee5d3..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,25 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local DANFOSS_LC13_THERMOSTAT_FPS = require "apiv6_bugfix.fingerprints" - - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION and not - (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[1].productType, - DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or - device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[2].productType, - DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) then - return true, require "apiv6_bugfix" - else - return false - end -end - -return can_handle diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua deleted file mode 100644 index e87a5990e2..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local DANFOSS_LC13_THERMOSTAT_FPS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - -return DANFOSS_LC13_THERMOSTAT_FPS diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua deleted file mode 100644 index 52c419a590..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,22 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-thermostat/src/init.lua b/drivers/SmartThings/zwave-thermostat/src/init.lua index 3668085b1a..6a79c1f4e1 100755 --- a/drivers/SmartThings/zwave-thermostat/src/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/init.lua @@ -106,6 +106,7 @@ local driver_template = { added = device_added }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua index dc30091b79..38b6d5de87 100644 --- a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua @@ -10,6 +10,5 @@ local sub_drivers = { lazy_load_if_possible("stelpro-ki-thermostat"), lazy_load_if_possible("qubino-flush-thermostat"), lazy_load_if_possible("thermostat-heating-battery"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-valve/src/init.lua b/drivers/SmartThings/zwave-valve/src/init.lua index cea5b877c1..f430c19a4a 100644 --- a/drivers/SmartThings/zwave-valve/src/init.lua +++ b/drivers/SmartThings/zwave-valve/src/init.lua @@ -17,6 +17,7 @@ local driver_template = { capabilities.valve, }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-window-treatment/src/init.lua b/drivers/SmartThings/zwave-window-treatment/src/init.lua index b2597d9f61..8b8f32c49b 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/init.lua @@ -75,6 +75,7 @@ local driver_template = { } }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index a67fdfeddf..d1a3ed3369 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -140,3 +140,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smart button MIR-SO100",麦乐克智能按钮MIR-SO100 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 +"Mirror Series 4x4 1",镜系列4x4 1 diff --git a/tools/run_driver_tests.py b/tools/run_driver_tests.py index e3e58f2154..e686a0f4b7 100755 --- a/tools/run_driver_tests.py +++ b/tools/run_driver_tests.py @@ -117,10 +117,10 @@ def run_tests(verbosity_level, filter, junit, coverage_files, html): test_status = "" test_logs = "" test_done = False - if re.match("^\s*$", line) is None: + if re.match(r"^\s*$", line) is None: last_line = line - m = re.match("Passed (\d+) of (\d+) tests", last_line) + m = re.match(r"Passed (\d+) of (\d+) tests", last_line) if m is None: failure_files[test_file].append("\n ".join(a.stderr.decode().split("\n"))) test_case = junit_xml.TestCase(test_suite.name)