Skip to content
Open
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
93 changes: 93 additions & 0 deletions drivers/DeepSmart/deepsmart/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a **SmartThings Edge Driver** written in Lua that integrates DEEPSMART KNX gateways (called "Wiser" bridges) with Samsung SmartThings. It discovers bridges via SSDP, communicates over HTTPS REST APIs, and translates KNX telegrams to SmartThings capability events.

There is no build system, package manager, or test suite — this is a pure source project deployed directly to the SmartThings Edge platform.

## Architecture

```
src/
├── init.lua # Driver entry point — registers capabilities, lifecycle/command handlers, starts the IP-check schedule
├── config.lua # Device type enums (AC=3, HEATER=4, NEWFAN=53, SWITCH=14, SLIDER=16, HUE=25), address type constants
├── discovery.lua # SSDP-based bridge discovery. Callback spawns bridge creation and device loading
├── lifecycles.lua # init/added/removed handlers for both bridge devices and child (edge) devices
├── commands.lua # Translates SmartThings capability commands → Wisers.control() calls with appropriate addrtypes
├── ssdp.lua # Raw UDP SSDP M-SEARCH broadcast, parses responses for UUID+IP
├── utils.lua # Socket builder (TCP+SSL via cosock), backoff generator, deep table comparison
├── selfSignedRoot.crt # CA certificate for bridge HTTPS connections
├── deepsmart/
│ ├── wisers.lua # Central orchestrator: bridge lifecycle (add/del/reload/refresh), KNX↔SmartThings translation, 2s poll loop
│ ├── api.lua # HTTPS REST client wrapping lunchbox.rest: load_config, load_dp2knx, load_dpenum, query, control
│ ├── devices.lua # Parses bridge JSON config into device/protocol/address index. Maps KNX group addresses → device DP IDs
│ ├── dp2knx.lua # Maps productId → device type + DP ID ↔ address-type associations
│ └── dpenum.lua # Enum value conversion: SmartThings values (cool/heat/auto) ↔ DEEPSMART integer values
└── lunchbox/
├── init.lua # Module re-export: RestClient, EventSource
├── rest.lua # Full-featured HTTPS client with connection pooling, retry/backoff, chunked transfer support
├── sse/eventsource.lua # Server-Sent Events client
└── util.lua # URL parsing helpers, read-only table proxy
```

## How It Works — Data Flow

### Discovery & Setup
1. User scans for devices → `discovery.start()` sends SSDP M-SEARCH for `DEEPSMART-ARM`
2. Each bridge UUID+IP is passed to `wisers.add_wiser()`, which creates a bridge device and fetches all configs (devices, dp2knx, dpenum) from the bridge's HTTPS API
3. Every 2 seconds, `wiser_loop()` polls the bridge for changed devices — this keeps device state in sync

### Command Path (SmartThings → KNX)
1. SmartThings app sends command → `commands.lua` handler
2. Handler determines `addrtypes` based on device type (e.g., AC mode = addrtype 1)
3. Calls `wisers.control(device, command, addrtypes)` which:
- Looks up the DP ID for the product + addrtype via `dp2knx`
- Finds the KNX send address(es) from `devices`
- Converts SmartThings values to DEEPSMART values via `dpenum`
- Posts `control` API call to bridge

### Response Path (KNX → SmartThings)
1. `wiser_loop()` polls `/homecontroller/api/v1/config/changeddevs` every 2s
2. For changed devices, calls `query_deviceid()` which reads all KNX feedback addresses
3. KNX values are translated back to SmartThings values via `dpenum` and `dp2knx`
4. `driver:set_switch()`, `driver:ac_report()`, etc. emit SmartThings capability events

### Device Addressing
- `device_network_id` format: `wiser:pid:id[_idx]` (e.g., `abc123:cekfhkz5:42_1`)
- One physical DEEPSMART device can map to multiple SmartThings child devices (`idx` suffix)
- Bridge devices have `parent_assigned_child_key == nil`; child devices always have one

## Key Files for Common Changes

- **Adding a new device type**: Add the enum to `config.lua`, create a profile YAML in `profiles/`, add PID mapping in `dp2knx.lua`, add command handling in `commands.lua`, add response handling in `wisers.lua:knx_response()`
- **Changing API endpoints**: Edit `src/deepsmart/api.lua`
- **Changing discovery behavior**: Edit `src/discovery.lua` or `src/ssdp.lua`
- **Changing device lifecycle behavior**: Edit `src/lifecycles.lua`

## SmartThings Platform Constraints

- Runs on SmartThings Edge Lua runtime — `require('st.driver')`, `require('st.capabilities')`, etc. are platform-provided
- Uses `cosock` for TCP/SSL sockets (not standard Lua socket)
- The `luncheon` library provides HTTP Request/Response objects
- Driver data is persisted via `device:set_field(key, value, {persist = true})`
- Bridge configuration (devices, dp2knx, dpenum) are stored as persisted fields on the bridge device

## Device Types and Their Capabilities

| Type | Enum | Profile | Capabilities |
|------|------|---------|--------------|
| AC | 3 | Ac.v1 | switch, temperatureMeasurement, thermostatHeatingSetpoint, airConditionerMode, airConditionerFanMode |
| Heater | 4 | Heater.v1 | thermostatMode, thermostatOperatingState, thermostatHeatingSetpoint, temperatureMeasurement |
| NewFan | 53 | Newfan.v1 | switch, airConditionerFanMode |
| Switch | 14 | Light.v1 | switch |
| Slider | 16 | Slider.v1 | switch, switchLevel |
| Hue | 25 | Hue.v1 | switch, switchLevel, colorTemperature |
| Bridge | — | Deepsmart-bridge.v1 | refresh |

## Scheduling

- **Every 600 seconds**: `check_ip()` SSDP scan to detect bridge IP changes (e.g., DHCP renewal)
- **Every 2 seconds**: `wiser_loop()` polls bridge for changed device states and keeps the poll connection alive
12 changes: 12 additions & 0 deletions drivers/DeepSmart/deepsmart/profiles/Curtain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Curtain.v1
components:
- id: main
capabilities:
- id: windowShade
version: 1
- id: windowShadeLevel
version: 1
- id: refresh
version: 1
categories:
- name: Blind
18 changes: 18 additions & 0 deletions drivers/DeepSmart/deepsmart/profiles/Hue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Hue.v1
components:
- id: main
capabilities:
- id: switch
version: 1
- id: switchLevel
version: 1
- id: colorTemperature
version: 1
- id: refresh
version: 1
categories:
- name: Light
metadata:
deviceType: Light
ocfDeviceType: oic.d.light
deviceTypeId: Light
14 changes: 14 additions & 0 deletions drivers/DeepSmart/deepsmart/profiles/Light.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Light.v1
components:
- id: main
capabilities:
- id: switch
version: 1
- id: refresh
version: 1
categories:
- name: Light
metadata:
deviceType: Light
ocfDeviceType: oic.d.light
deviceTypeId: Light
16 changes: 16 additions & 0 deletions drivers/DeepSmart/deepsmart/profiles/Slider.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Slider.v1
components:
- id: main
capabilities:
- id: switch
version: 1
- id: switchLevel
version: 1
- id: refresh
version: 1
categories:
- name: Light
metadata:
deviceType: Light
ocfDeviceType: oic.d.light
deviceTypeId: Light
92 changes: 91 additions & 1 deletion drivers/DeepSmart/deepsmart/src/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function command_handler.set_switch(_, device, command)
local devtype = wisers.get_dev_type(device)
local addrtypes = {}
-- get devtype onoff addrtype
if (devtype == config.ENUM.AC or devtype == config.ENUM.HEATER or devtype == config.ENUM.NEWFAN) then
if (devtype == config.ENUM.AC or devtype == config.ENUM.HEATER or devtype == config.ENUM.NEWFAN or devtype == config.ENUM.SWITCH or devtype == config.ENUM.SLIDER or devtype == config.ENUM.HUE) then
addrtypes[1] = config.DEVICE.ONOFF
else
addrtypes[1] = 0
Expand All @@ -61,6 +61,40 @@ function command_handler.set_switch(_, device, command)
return 0
end

----------------
-- Switch level command
----------------
function command_handler.set_level(_, device, command)
local lvl = command.args.level
local success = wisers.control(device, command, {config.SLIDER.BRIGHT})
-- Check if success
if success then
if lvl == 0 then
device:emit_event(caps.switch.switch.off())
else
device:emit_event(caps.switch.switch.on())
end
device:emit_event(caps.switchLevel.level(lvl))
return
end
log.error('no response from device')
end

----------------
-- Color control command
----------------
function command_handler.set_color(_, device, command)
local success = wisers.control(device, command, {config.HUE.HUE})

-- Check if success
if success then
device:emit_event(caps.switch.switch.on())
device:emit_event(caps.colorTemperature.colorTemperature(command.args.temperature))
return
end
log.error('no response from device')
end

----------------
-- fan mode command
----------------
Expand Down Expand Up @@ -133,5 +167,61 @@ function command_handler.set_setheatingpoint(driver, device, command)
log.error('no response from device')
end

----------------
-- Window Shade command (open/close/pause)
----------------
function command_handler.set_shade(_, device, command)
local cmd = command.command
log.info('hub control device '..device.parent_assigned_child_key..' shade '..cmd)
-- determine addrtypes based on command
local addrtypes = {}
if (cmd == 'open') then
addrtypes[1] = config.CURTAIN.OPEN
addrtypes[2] = config.CURTAIN.CLOSE
elseif (cmd == 'close') then
addrtypes[1] = config.CURTAIN.OPEN
addrtypes[2] = config.CURTAIN.CLOSE
elseif (cmd == 'pause') then
addrtypes[1] = config.CURTAIN.PAUSE
else
addrtypes[1] = config.CURTAIN.PAUSE
end
local success = wisers.control(device, command, addrtypes)
if success then
device:online()
if cmd == 'close' then
return device:emit_event(caps.windowShade.windowShade.closed())
elseif cmd == 'open' then
return device:emit_event(caps.windowShade.windowShade.open())
elseif cmd == 'pause' then
return device:emit_event(caps.windowShade.windowShade.paused())
end
-- pause: no state change, just acknowledge
return
end
log.error('no response from device')
return 0
end

----------------
-- Window Shade Level command
----------------
function command_handler.set_shade_level(_, device, command)
local level = command.args.shadeLevel
log.info('hub control device '..device.parent_assigned_child_key..' shade level '..level)
local success = wisers.control(device, command, {config.CURTAIN.LEVEL})
if success then
device:online()
device:emit_event(caps.windowShadeLevel.shadeLevel(level))
if level == 0 then
device:emit_event(caps.windowShade.windowShade.closed())
elseif level == 100 then
device:emit_event(caps.windowShade.windowShade.open())
end
return
end
log.error('no response from device')
end


return command_handler
29 changes: 28 additions & 1 deletion drivers/DeepSmart/deepsmart/src/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ local config = {}
config.DEVICE_PROFILE={}
config.DEVICE_PROFILE[3]='Ac.v1'
config.DEVICE_PROFILE[4]='Heater.v1'
config.DEVICE_PROFILE[14]='Light.v1'
config.DEVICE_PROFILE[16]='Slider.v1'
config.DEVICE_PROFILE[39]='Slider.v1'
config.DEVICE_PROFILE[25]='Hue.v1'
config.DEVICE_PROFILE[38]='Hue.v1'
config.DEVICE_PROFILE[53]='Newfan.v1'
config.DEVICE_PROFILE[15]='Curtain.v1'
config.DEVICE_TYPE='LAN'

-- SSDP Config
Expand All @@ -18,7 +24,10 @@ config.ENUM = {}
config.ENUM.AC = 3
config.ENUM.HEATER = 4
config.ENUM.NEWFAN = 53

config.ENUM.SWITCH = 14
config.ENUM.SLIDER = 39
config.ENUM.HUE = 38
config.ENUM.CURTAIN = 15

--device addrtype
config.AC = {}
Expand All @@ -37,6 +46,24 @@ config.NEWFAN = {}
config.NEWFAN.ONOFF = 0
config.NEWFAN.FAN = 1

config.SWITCH = {}
config.SWITCH.ONOFF = 0

config.SLIDER = {}
config.SLIDER.ONOFF = 0
config.SLIDER.BRIGHT = 1

config.HUE = {}
config.HUE.ONOFF = 0
config.HUE.BRIGHT = 1
config.HUE.HUE = 2

config.CURTAIN = {}
config.CURTAIN.PAUSE= 0
config.CURTAIN.OPEN = 1
config.CURTAIN.CLOSE = 2
config.CURTAIN.LEVEL = 3

config.DEVICE = {}
config.DEVICE.ONOFF = 0

Expand Down
16 changes: 7 additions & 9 deletions drivers/DeepSmart/deepsmart/src/deepsmart/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function Api.client(wiser_index_code, ip)
local ret = setmetatable({
wiser_index_code = wiser_index_code,
ip = ip,
client = RestClient.new("https://"..ip, utils.labeled_socket_builder(wiser_index_code, SSL_CONFIG))
client = nil
}, Api)

return ret
Expand All @@ -51,23 +51,21 @@ end

function Api:do_get(url)
-- get url
local client = self.client
local client = RestClient.new("https://"..self.ip, utils.labeled_socket_builder(self.wiser_index_code, SSL_CONFIG))
if (client == nil) then
log.warn('do_get url '..url..' client is nil')
return nil,'client nil',404
end
log.debug('do_get '..url)
local response,err,partial = client:get(url, ADDITIONAL_HEADERS, retry_fn(3))
if (err ~= nil) then
client:shutdown()
self.client = RestClient.new("https://"..self.ip, utils.labeled_socket_builder(self.wiser_index_code, SSL_CONFIG))
end
client:shutdown()
client = nil
return process_rest_response(response,err,partial)
end

function Api:do_post(url, content)
-- get url
local client = self.client
local client = RestClient.new("https://"..self.ip, utils.labeled_socket_builder(self.wiser_index_code, SSL_CONFIG))
if (client == nil) then
log.warn('do_post url '..url..' client is nil')
return nil,'client nil',404
Expand All @@ -76,15 +74,15 @@ function Api:do_post(url, content)
local response,err,partial = client:post(url, content, ADDITIONAL_HEADERS, retry_fn(3))
if (err ~= nil) then
log.warn('post url '..url..' content '..content..' error '..err)
client:shutdown()
self.client = RestClient.new("https://"..self.ip, utils.labeled_socket_builder(self.wiser_index_code, SSL_CONFIG))
else
if (response == nil or response:get_body() == nil) then
log.warn('post url '..url..' content '..content..' res nil')
else
log.trace('post url '..url..' content '..content..' res '..response:get_body())
end
end
client:shutdown()
client = nil
return process_rest_response(response,err,partial)
end

Expand Down
4 changes: 2 additions & 2 deletions drivers/DeepSmart/deepsmart/src/deepsmart/devices.lua
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ function Devices:get_dev_dpid_addr(dev_id, dpid)
log.trace('add recv addr '..v.addr)
feedback_list[v.addr] = v
end
log.trace('sendlist count '..#send_list..' feedback_list count '..#feedback_list)
return send_list,feedback_list
end
log.trace('sendlist count '..#send_list..' feedback_list count '..#feedback_list)
return send_list,feedback_list
end
end
return nil,nil
Expand Down
Loading
Loading