Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions drivers/EconetControlsInc/bulldog-gatelock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 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:

1. Adds `kTamperDetected (10)` to `GeneralDiagnostics.ActiveHardwareFaults` on endpoint 0 (also emits the `HardwareFaultChange` event).
2. Fires a legacy `DoorLockAlarm` event with `alarmCode = kWrongCodeEntryLimit (4)` for backwards compatibility.

The driver subscribes to the `ActiveHardwareFaults` attribute and maps list membership directly to the **tamperAlert** capability — `tampered` while the list contains `10`, `clear` when the firmware removes it (which happens automatically when the lockout window expires). The legacy `DoorLockAlarm` event handler is retained so older firmware builds that only fire the event still surface a `tampered` 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 <driver-id> <version> -C <channel-id>
smartthings edge:drivers:install <driver-id> --hub <hub-id> -C <channel-id>
```

## Re-deploy after edits

After every code change:

```bash
# Package + auto-assign + install
smartthings edge:drivers:package -C <channel-id> --hub <hub-id>

# If the hub doesn't pick up the new version (cached), force re-install:
smartthings edge:drivers:install <driver-id> --hub <hub-id> -C <channel-id>
```

## Live logs

```bash
smartthings edge:drivers:logcat <driver-id>
```

## 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.
6 changes: 6 additions & 0 deletions drivers/EconetControlsInc/bulldog-gatelock/config.yml
Original file line number Diff line number Diff line change
@@ -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: {}
7 changes: 7 additions & 0 deletions drivers/EconetControlsInc/bulldog-gatelock/fingerprints.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
matterManufacturer:
- id: "econet-gatelock"
deviceProfileName: gatelock-matter
vendorId: 0x1568 # Econet Controls Inc (5480)
productId: 0x000A # 10 (Bulldog GateLock)
deviceTypes:
- id: 0x000A # MA-doorlock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: gatelock-matter
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
184 changes: 184 additions & 0 deletions drivers/EconetControlsInc/bulldog-gatelock/src/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
-- 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 GeneralDiagnostics = clusters.GeneralDiagnostics

local UNLATCHED_STATE = 0x3
local HARDWARE_FAULT_TAMPER_DETECTED = 10

----------------------------------------------------------------------
-- 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

-- GeneralDiagnostics.ActiveHardwareFaults is a list of HardwareFaultEnum values.
-- Firmware adds kTamperDetected (10) when the keypad 4-strike brute-force
-- limit trips and removes it when the lockout expires. Map list membership
-- directly to the tamperAlert capability so the SmartThings UI tracks the
-- attribute's full lifecycle (detected -> clear) instead of just the alarm
-- event edge.
local function hardware_faults_handler(driver, device, ib, response)
local list = ib.data and ib.data.elements
local tampered = false
if list ~= nil then
for _, entry in ipairs(list) do
if entry.value == HARDWARE_FAULT_TAMPER_DETECTED then
tampered = true
break
end
end
end
if tampered then
device:emit_event(capabilities.tamperAlert.tamper.detected())
else
device:emit_event(capabilities.tamperAlert.tamper.clear())
end
end

----------------------------------------------------------------------
-- EVENT HANDLERS
----------------------------------------------------------------------

-- Retained for compatibility with firmware that only fires the
-- DoorLockAlarm event (older builds without GeneralDiagnostics tamper
-- reporting). On builds that report both, the attribute handler above
-- supersedes this by also clearing the state.
local function door_lock_alarm_handler(driver, device, ib, response)
device:emit_event(capabilities.tamperAlert.tamper.detected())
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,
},
[GeneralDiagnostics.ID] = {
[GeneralDiagnostics.attributes.ActiveHardwareFaults.ID] = hardware_faults_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,
},
[capabilities.tamperAlert.ID] = {
GeneralDiagnostics.attributes.ActiveHardwareFaults,
},
},

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()
Loading