diff --git a/drivers/EconetControlsInc/bulldog-gatelock/README.md b/drivers/EconetControlsInc/bulldog-gatelock/README.md new file mode 100644 index 0000000000..8a9a712024 --- /dev/null +++ b/drivers/EconetControlsInc/bulldog-gatelock/README.md @@ -0,0 +1,80 @@ +# Econet GateLock — SmartThings Edge Driver (Matter) + +Custom SmartThings Edge driver for the Econet Bulldog GateLock. Built on the Matter-specific `st.matter.driver` class so the secure Matter session (`matter_channel`) is established per device. + +## Capabilities Exposed + +| SmartThings Capability | Matter Source | What it shows | +|---|---|---| +| **lock** | DoorLock cluster, LockState (attr 0x0000) | Locked / Unlocked / Not Fully Locked | +| **contactSensor** | DoorLock cluster, DoorState (attr 0x0003) | Door Open / Closed (driven by reed switch) | +| **tamperAlert** | DoorLock cluster, DoorLockAlarm event | Tampered when 4-strike PIN limit hit on the keypad | +| **battery** | PowerSource cluster, BatPercentRemaining (attr 0x000C) | 0–100% | +| **firmwareUpdate** | (infrastructure) | Required for Matter device handshake | +| **refresh** | (infrastructure) | Manual re-subscribe from the SmartThings app | + +PIN management and auto-relock configuration are not exposed by this driver. PINs are managed on the lock's keypad in admin mode; auto-relock can be set via Matter's standard cluster from any other Matter controller (or the firmware shell). + +## Reed-switch contact sensor + +The reed switch on GPIO0.28 triggers `sendDoorStateChangeAlarmEvent()` in firmware, which updates the Matter `DoorState` attribute. This driver maps it to the SmartThings **Contact Sensor** tile: +- `DoorClosed (1)` → **closed** +- Anything else → **open** + +## Tamper alert + +When the keypad's 4-strikes-in-20-seconds brute-force protection trips, the firmware fires a `DoorLockAlarm` event with `alarmCode = kWrongCodeEntryLimit (4)`. The driver maps this to the standard **tamperAlert** capability ("tampered" badge in the app) and auto-clears the state after 15 seconds, slightly longer than the firmware's ~10-second keypad lockout window. On `device_added` the driver also emits `clear` to ensure a known initial state. + +## Prerequisites + +- SmartThings Hub with Matter support (v46+ firmware) +- SmartThings CLI (`@smartthings/cli`) +- Personal Access Token from https://account.smartthings.com/tokens (set as `SMARTTHINGS_TOKEN` env var) + +## Build & Deploy + +```bash +cd smartthings-edge-driver +smartthings edge:drivers:package +# Returns a driver ID + +smartthings edge:channels:assign -C +smartthings edge:drivers:install --hub -C +``` + +## Re-deploy after edits + +After every code change: + +```bash +# Package + auto-assign + install +smartthings edge:drivers:package -C --hub + +# If the hub doesn't pick up the new version (cached), force re-install: +smartthings edge:drivers:install --hub -C +``` + +## Live logs + +```bash +smartthings edge:drivers:logcat +``` + +## File structure + +``` +smartthings-edge-driver/ +├── config.yml # Driver metadata +├── fingerprints.yml # Matter vendor 5480 / product 10 match +├── profiles/ +│ └── gatelock-matter.yml # Capability list +├── src/ +│ └── init.lua # Driver code (uses st.matter.driver) +└── README.md +``` + +## Notes + +- The driver MUST use `MatterDriver = require "st.matter.driver"` and instantiate via `MatterDriver(packageKey, driverTable)`. The generic `st.driver` does not establish the Matter secure session and `device:subscribe()` will fail with `matter_channel nil`. +- `subscribed_attributes` is keyed by SmartThings capability ID, with values being arrays of cluster attribute object refs (not raw numeric IDs). +- `subscribed_events` follows the same pattern keyed by capability ID. diff --git a/drivers/EconetControlsInc/bulldog-gatelock/config.yml b/drivers/EconetControlsInc/bulldog-gatelock/config.yml new file mode 100644 index 0000000000..d740be2bd9 --- /dev/null +++ b/drivers/EconetControlsInc/bulldog-gatelock/config.yml @@ -0,0 +1,6 @@ +name: "Econet GateLock Matter" +packageKey: "econet-gatelock-matter" +description: "SmartThings Edge driver for the Econet Bulldog GateLock (Matter). Exposes lock control, door open/close contact sensor, tamper alert, and battery." +permissions: + matter: {} +lifecycle: {} diff --git a/drivers/EconetControlsInc/bulldog-gatelock/fingerprints.yml b/drivers/EconetControlsInc/bulldog-gatelock/fingerprints.yml new file mode 100644 index 0000000000..90a0b32a57 --- /dev/null +++ b/drivers/EconetControlsInc/bulldog-gatelock/fingerprints.yml @@ -0,0 +1,8 @@ +matterManufacturer: + - id: "econet-gatelock" + deviceLabel: Bulldog GateLock + deviceProfileName: matter-lock-contact-tamper + vendorId: 0x1568 # Econet Controls Inc (5480) + productId: 0x000A # 10 (Bulldog GateLock) + deviceTypes: + - id: 0x000A # MA-doorlock diff --git a/drivers/EconetControlsInc/bulldog-gatelock/profiles/matter-lock-contact-tamper.yml b/drivers/EconetControlsInc/bulldog-gatelock/profiles/matter-lock-contact-tamper.yml new file mode 100644 index 0000000000..9f07f8c9f6 --- /dev/null +++ b/drivers/EconetControlsInc/bulldog-gatelock/profiles/matter-lock-contact-tamper.yml @@ -0,0 +1,18 @@ +name: matter-lock-contact-tamper +components: + - id: main + capabilities: + - id: lock + version: 1 + - id: contactSensor + version: 1 + - id: tamperAlert + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/EconetControlsInc/bulldog-gatelock/src/init.lua b/drivers/EconetControlsInc/bulldog-gatelock/src/init.lua new file mode 100644 index 0000000000..cb1ffaaf59 --- /dev/null +++ b/drivers/EconetControlsInc/bulldog-gatelock/src/init.lua @@ -0,0 +1,156 @@ +-- Econet GateLock Matter Edge Driver. +-- +-- Built on st.matter.driver (NOT the generic st.driver) — this is the +-- Matter-specific driver class that actually attaches the secure +-- matter_channel session to each device. Using the generic Driver class +-- causes "matter_channel nil" because no Matter subsystem hookup happens. + +local MatterDriver = require "st.matter.driver" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" + +local DoorLock = clusters.DoorLock +local PowerSource = clusters.PowerSource + +local UNLATCHED_STATE = 0x3 + +---------------------------------------------------------------------- +-- ATTRIBUTE HANDLERS +---------------------------------------------------------------------- + +local function lock_state_handler(driver, device, ib, response) + local LockState = DoorLock.attributes.LockState + local attr = capabilities.lock.lock + local map = { + [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), + [LockState.LOCKED] = attr.locked(), + [LockState.UNLOCKED] = attr.unlocked(), + [UNLATCHED_STATE] = attr.unlocked(), + } + if ib.data.value ~= nil and map[ib.data.value] then + device:emit_event(map[ib.data.value]) + else + device:emit_event(attr.not_fully_locked()) + end +end + +local function door_state_handler(driver, device, ib, response) + local val = ib.data.value + if val == nil then return end + if val == 1 then + device:emit_event(capabilities.contactSensor.contact.closed()) + else + device:emit_event(capabilities.contactSensor.contact.open()) + end +end + +local function battery_percent_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +---------------------------------------------------------------------- +-- EVENT HANDLERS +---------------------------------------------------------------------- + +-- Firmware locks out the keypad for ~10 s on the 4-strike brute-force trip. +-- Auto-clear after 15 s (10 s lockout + 5 s buffer) so SmartThings tracks +-- a real detected->clear transition without waiting for a driver restart. +local TAMPER_CLEAR_DELAY_S = 15 + +local function door_lock_alarm_handler(driver, device, ib, response) + device:emit_event(capabilities.tamperAlert.tamper.detected()) + device.thread:call_with_delay(TAMPER_CLEAR_DELAY_S, function() + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end) +end + +---------------------------------------------------------------------- +-- COMMAND HANDLERS +---------------------------------------------------------------------- + +local function handle_lock(driver, device, command) + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.LockDoor(device, ep)) +end + +local function handle_unlock(driver, device, command) + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.UnlockDoor(device, ep)) +end + +local function handle_refresh(driver, device, command) + device:refresh() +end + +---------------------------------------------------------------------- +-- LIFECYCLE +---------------------------------------------------------------------- + +local function device_init(driver, device) + device:subscribe() +end + +local function device_added(driver, device) + device:emit_event(capabilities.tamperAlert.tamper.clear()) +end + +---------------------------------------------------------------------- +-- DRIVER TABLE (passed as 2nd arg to MatterDriver) +---------------------------------------------------------------------- + +local matter_lock_driver = { + lifecycle_handlers = { + init = device_init, + added = device_added, + }, + + matter_handlers = { + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.LockState.ID] = lock_state_handler, + [DoorLock.attributes.DoorState.ID] = door_state_handler, + }, + [PowerSource.ID] = { + [PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_handler, + }, + }, + event = { + [DoorLock.ID] = { + [DoorLock.events.DoorLockAlarm.ID] = door_lock_alarm_handler, + }, + }, + }, + + subscribed_attributes = { + [capabilities.lock.ID] = { + DoorLock.attributes.LockState, + }, + [capabilities.contactSensor.ID] = { + DoorLock.attributes.DoorState, + }, + [capabilities.battery.ID] = { + PowerSource.attributes.BatPercentRemaining, + }, + }, + + subscribed_events = { + [capabilities.tamperAlert.ID] = { + DoorLock.events.DoorLockAlarm, + }, + }, + + capability_handlers = { + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = handle_lock, + [capabilities.lock.commands.unlock.NAME] = handle_unlock, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = handle_refresh, + }, + }, +} + +local matter_driver = MatterDriver("econet-gatelock-matter", matter_lock_driver) +matter_driver:run()