diff --git a/drivers/DeepSmart/deepsmart/CLAUDE.md b/drivers/DeepSmart/deepsmart/CLAUDE.md new file mode 100644 index 0000000000..67a03c4cf7 --- /dev/null +++ b/drivers/DeepSmart/deepsmart/CLAUDE.md @@ -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 diff --git a/drivers/DeepSmart/deepsmart/profiles/Curtain.yml b/drivers/DeepSmart/deepsmart/profiles/Curtain.yml new file mode 100644 index 0000000000..8a8d90203f --- /dev/null +++ b/drivers/DeepSmart/deepsmart/profiles/Curtain.yml @@ -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 diff --git a/drivers/DeepSmart/deepsmart/profiles/Hue.yml b/drivers/DeepSmart/deepsmart/profiles/Hue.yml new file mode 100644 index 0000000000..c9f72b69a7 --- /dev/null +++ b/drivers/DeepSmart/deepsmart/profiles/Hue.yml @@ -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 diff --git a/drivers/DeepSmart/deepsmart/profiles/Light.yml b/drivers/DeepSmart/deepsmart/profiles/Light.yml new file mode 100644 index 0000000000..7cea315f7a --- /dev/null +++ b/drivers/DeepSmart/deepsmart/profiles/Light.yml @@ -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 diff --git a/drivers/DeepSmart/deepsmart/profiles/Slider.yml b/drivers/DeepSmart/deepsmart/profiles/Slider.yml new file mode 100644 index 0000000000..0c66d62986 --- /dev/null +++ b/drivers/DeepSmart/deepsmart/profiles/Slider.yml @@ -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 diff --git a/drivers/DeepSmart/deepsmart/src/commands.lua b/drivers/DeepSmart/deepsmart/src/commands.lua index 20ebdc4fec..dab3acee21 100644 --- a/drivers/DeepSmart/deepsmart/src/commands.lua +++ b/drivers/DeepSmart/deepsmart/src/commands.lua @@ -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 @@ -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 ---------------- @@ -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 diff --git a/drivers/DeepSmart/deepsmart/src/config.lua b/drivers/DeepSmart/deepsmart/src/config.lua index 46a958d313..c49c037a93 100644 --- a/drivers/DeepSmart/deepsmart/src/config.lua +++ b/drivers/DeepSmart/deepsmart/src/config.lua @@ -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 @@ -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 = {} @@ -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 diff --git a/drivers/DeepSmart/deepsmart/src/deepsmart/api.lua b/drivers/DeepSmart/deepsmart/src/deepsmart/api.lua index fc593b63f4..e5a72fa74c 100644 --- a/drivers/DeepSmart/deepsmart/src/deepsmart/api.lua +++ b/drivers/DeepSmart/deepsmart/src/deepsmart/api.lua @@ -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 @@ -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 @@ -76,8 +74,6 @@ 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') @@ -85,6 +81,8 @@ function Api:do_post(url, content) 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 diff --git a/drivers/DeepSmart/deepsmart/src/deepsmart/devices.lua b/drivers/DeepSmart/deepsmart/src/deepsmart/devices.lua index d9e06be339..28ae098725 100644 --- a/drivers/DeepSmart/deepsmart/src/deepsmart/devices.lua +++ b/drivers/DeepSmart/deepsmart/src/deepsmart/devices.lua @@ -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 diff --git a/drivers/DeepSmart/deepsmart/src/deepsmart/dp2knx.lua b/drivers/DeepSmart/deepsmart/src/deepsmart/dp2knx.lua index 62ab6c07bc..172f0d661b 100644 --- a/drivers/DeepSmart/deepsmart/src/deepsmart/dp2knx.lua +++ b/drivers/DeepSmart/deepsmart/src/deepsmart/dp2knx.lua @@ -7,7 +7,12 @@ Dp2Knx.__index = Dp2Knx Dp2Knx.pids = { ['cekfhkz5'] = {pid='cekfhkz5',type=config.ENUM.AC, devs={[1]={[1]=0,[4]=1,[5]=2,[2]=3,[3]=4,[101]=5}}}, ['drrearrq'] = {pid='drrearrq',type=config.ENUM.HEATER, devs={[1]={[1]=0,[24]=2,[16]=3}}}, - ['w35ineur'] = {pid='w35ineur',type=config.ENUM.NEWFAN, devs={[1]={[1]=0,[12]=1}}} + ['w35ineur'] = {pid='w35ineur',type=config.ENUM.NEWFAN, devs={[1]={[1]=0,[12]=1}}}, + ['fzjliiuu'] = {pid='fzjliiuu',type=config.ENUM.SWITCH, devs={[1]={[1]=0}}}, + ['a2u6yabn'] = {pid='a2u6yabn',type=config.ENUM.SLIDER, devs={[1]={[101]=0,[102]=1}}}, + ['uyyvbtjs'] = {pid='uyyvbtjs',type=config.ENUM.HUE, devs={[1]={[101]=0,[102]=1,[24]=2}}}, + ['oruqmmxg'] = {pid='oruqmmxg',type=config.ENUM.HUE, devs={[1]={[101]=0,[102]=1,[24]=2}}}, + ['5bempufw'] = {pid='5bempufw',type=config.ENUM.CURTAIN, devs={[1]={[4]=0,[2]=3,[1]=1}}} } ------------------ -- load wiser dp2knx config diff --git a/drivers/DeepSmart/deepsmart/src/deepsmart/wisers.lua b/drivers/DeepSmart/deepsmart/src/deepsmart/wisers.lua index 562a63c616..b4ee39dc52 100644 --- a/drivers/DeepSmart/deepsmart/src/deepsmart/wisers.lua +++ b/drivers/DeepSmart/deepsmart/src/deepsmart/wisers.lua @@ -76,13 +76,8 @@ function Wisers.update_wiser_ip(uuid, ip) end if (wiser.ip ~= ip) then log.info('wiser '..uuid..' change ip to '..(ip or '')) + wiser.bridge:set_field(config.FIELD.IP, ip, { persist = true}) wiser["ip"] = ip - if (wiser.api ~= nil) then - wiser.api:shutdown() - end - if (wiser.loopapi ~= nil) then - wiser.loopapi:shutdown() - end wiser["api"] = api.client(uuid, ip) wiser["loopapi"] = api.client(uuid, ip) end @@ -303,8 +298,12 @@ function Wisers.add_wiser(uuid, ip, bridge) local wiser = Wisers.get_wiser(uuid) -- first check wiser ip, if ip is same then do nothing -- in case the wiser's ip changed by dhcp or router restart or router changed or mannal static ip change - if (wiser ~= nil and wiser.ip == ip) then - log.trace('wiser '..uuid..' is same') + if (wiser ~= nil) then + if (wiser.ip == ip) then + log.trace('wiser '..uuid..' is same') + else + Wisers.update_wiser_ip(uuid, ip) + end return end -- if wiser is new then create new wiser @@ -448,7 +447,7 @@ function Wisers.reload(uuid, add_devs, del_devs) end -- get devices local configret,configstr = deepsmartapi:load_config() - log.trace(string.format('load config (%s)', configstr)) + log.trace(string.format('load config (%s) ret(%s)', configstr, tostring(configret))) if (configret and configstr ~= wiser.bridge:get_field(config.FIELD.DEVICES)) then local old_config = wiser.config wiser["config"] = Devices.load_config(wiser.wiser_index_code, configstr, old_config, add_devs, del_devs) @@ -456,12 +455,14 @@ function Wisers.reload(uuid, add_devs, del_devs) end -- get dp info local dp2knxret,dp2knx_config = deepsmartapi:load_dp2knx() + log.trace(string.format('load dp2knx (%s) ret(%s)', dp2knx_config, tostring(dp2knxret))) if (dp2knxret and dp2knx_config ~= wiser.bridge:get_field(config.FIELD.DP2KNX)) then wiser["dp2knx"] = dp2knx.load_config(wiser.wiser_index_code, dp2knx_config) wiser.bridge:set_field(config.FIELD.DP2KNX, dp2knx_config, { persist = true}) end -- get enum info local dpenumret,dpenum_config = deepsmartapi:load_dpenum() + log.trace(string.format('load dpenum (%s) ret(%s)', dpenum_config, tostring(dpenumret))) if (dpenumret and dpenum_config ~= wiser.bridge:get_field(config.FIELD.DPENUM)) then wiser["dpenum"] = dpenum.load_config(wiser.wiser_index_code, dpenum_config) wiser.bridge:set_field(config.FIELD.DPENUM, dpenum_config, { persist = true}) @@ -542,7 +543,13 @@ function Wisers.knx_response(wiser_index_code, knxes) end if (devtype ~= nil and addrtype ~= nil) then log.trace('dev '..dev_id..' pid '..device.productId..' convert to devtype '..devtype..' addrtype '..addrtype) - if (devtype == config.ENUM.AC) then + if (devtype == config.ENUM.SWITCH) then + local on_off = 'off' + if (value ~= 0) then + on_off = 'on' + end + Wisers.driver:set_switch(dev, on_off) + elseif (devtype == config.ENUM.AC) then if (addrtype == 0) then local on_off = 'off' if (value ~= 0) then @@ -583,6 +590,40 @@ function Wisers.knx_response(wiser_index_code, knxes) local fan = wiser.dpenum:get_dev_val_by_pid_val(device.productId, dpid, value) Wisers.driver:ac_report(dev, nil,nil,fan,nil, nil,nil) end + elseif (devtype == config.ENUM.SLIDER) then + if (addrtype == 0) then + local on_off = 'off' + if (value ~= 0) then + on_off = 'on' + end + Wisers.driver:set_switch(dev, on_off) + elseif (addrtype == 1) then + Wisers.driver:set_level(dev, value*100//255) + end + elseif (devtype == config.ENUM.HUE) then + if (addrtype == 0) then + local on_off = 'off' + if (value ~= 0) then + on_off = 'on' + end + Wisers.driver:set_switch(dev, on_off) + elseif (addrtype == 1) then + Wisers.driver:set_level(dev, value*100//255) + elseif (addrtype == 2) then + Wisers.driver:set_hue(dev, value) + end + elseif (devtype == config.ENUM.CURTAIN) then + if (addrtype == 1 or addrtype == 2) then + local on_off = 'close' + if (value ~= 0) then + on_off = 'open' + end + Wisers.driver:curtain_report(dev, on_off, nil, nil) + elseif (addrtype == 0) then + Wisers.driver:curtain_report(dev, "pause", nil, nil) + elseif (addrtype == 3) then + Wisers.driver:curtain_report(dev, nil, (128+value*100)//255, nil) + end end else log.trace('pid '..device.productId..' dpid '..dpid..' convert to devtype addrtype nil') @@ -599,15 +640,27 @@ end -------------- function Wisers.get_dev_type(device) log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype') - if (device.profile.components.main.capabilities.airConditionerMode ~= nil) then + if (device.profile.components.main.capabilities.windowShade ~= nil) then + log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype curtain') + return config.ENUM.CURTAIN + elseif (device.profile.components.main.capabilities.airConditionerMode ~= nil) then log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype ac') return config.ENUM.AC elseif (device.profile.components.main.capabilities.thermostatMode ~= nil) then log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype heater') return config.ENUM.HEATER - else + elseif (device.profile.components.main.capabilities.airConditionerFanMode ~= nil) then log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype newfan') return config.ENUM.NEWFAN + elseif (device.profile.components.main.capabilities.colorTemperature ~= nil) then + log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype hue') + return config.ENUM.HUE + elseif (device.profile.components.main.capabilities.switchLevel~= nil) then + log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype slider') + return config.ENUM.SLIDER + else + log.trace('get device '..(device.parent_assigned_child_key or 'nil')..' devtype switch') + return config.ENUM.SWITCH end end @@ -630,12 +683,23 @@ function Wisers.default_report(driver, device) end local devtype = wiser.dp2knx:get_pid_type(dev.productId) log.trace('dev '..dev.productId..' get type '..devtype) - if (devtype == config.ENUM.AC) then + if (devtype == config.ENUM.SWITCH) then + driver:set_switch(device, "off") + elseif (devtype == config.ENUM.SLIDER) then + driver:set_switch(device, "off") + driver:set_level(device, 0) + elseif (devtype == config.ENUM.HUE) then + driver:set_switch(device, "off") + driver:set_level(device, 0) + driver:set_hue(device, 0) + elseif (devtype == config.ENUM.AC) then driver:ac_report(device, "off", "auto", "auto", 25, 25, nil) elseif (devtype == config.ENUM.HEATER) then driver:ac_report(device, "off", nil, nil, 25, 25, nil) elseif (devtype == config.ENUM.NEWFAN) then driver:ac_report(device, "off", nil, "low", nil,nil, nil) + elseif (devtype == config.ENUM.CURTAIN) then + driver:curtain_report(device, "closed", 0, nil) end return true,nil end @@ -778,6 +842,24 @@ function Wisers.control(device, command, addrtypes) deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) end ret = true + elseif (capability == 'switchLevel') then + local value = command.args.level*255//100 + for i,addr in pairs(send_list) do + log.info('device '..uuid..' control to addr '..addr.addr_int..' value '..value) + wiser.ctrlmap[addr.addr_int] = value + deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) + end + ret = true + elseif (capability == 'colorTemperature') then + -- samsung color temperature is 1-30000 + -- deepsmart color temperature is 0-255 + local value = (command.args.temperature-1)*255//29999 + for i,addr in pairs(send_list) do + log.info('device '..uuid..' control to addr '..addr.addr_int..' value '..value) + wiser.ctrlmap[addr.addr_int] = value + deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) + end + ret = true elseif (capability == 'airConditionerMode') then local mode = wiser.dpenum:get_pid_val_by_dev_val(dev.productId, dpid, command.args.mode) if (mode ~= nil) then @@ -822,6 +904,35 @@ function Wisers.control(device, command, addrtypes) deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) end ret = true + elseif (capability == 'windowShade') then + if (cmd == 'pause') then + -- pause sends value 1 to stop addr + for i,addr in pairs(send_list) do + log.info('device '..uuid..' shade pause to addr '..addr.addr_int) + wiser.ctrlmap[addr.addr_int] = 1 + deepsmartapi:control(addr.addr_int, addr.dataType, 1, 0) + end + else + local value = 0 + if (cmd == 'open') then + value = 1 + end + for i,addr in pairs(send_list) do + log.info('device '..uuid..' control to addr '..addr.addr_int..' value '..value) + wiser.ctrlmap[addr.addr_int] = value + deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) + end + end + ret = true + elseif (capability == 'windowShadeLevel') then + -- SmartThings level is 0-100, deepsmart level is 0-255 + local value = command.args.shadeLevel*255//100 + for i,addr in pairs(send_list) do + log.info('device '..uuid..' control to addr '..addr.addr_int..' value '..value) + wiser.ctrlmap[addr.addr_int] = value + deepsmartapi:control(addr.addr_int, addr.dataType, value, 0) + end + ret = true end ::addrtype_retry:: end diff --git a/drivers/DeepSmart/deepsmart/src/init.lua b/drivers/DeepSmart/deepsmart/src/init.lua index 037227c3d4..da146635d0 100644 --- a/drivers/DeepSmart/deepsmart/src/init.lua +++ b/drivers/DeepSmart/deepsmart/src/init.lua @@ -17,12 +17,16 @@ Driver( lifecycle_handlers = lifecycles, supported_capabilities = { caps.switch, + caps.switchLevel, + caps.colorTemperature, caps.refresh, caps.airConditionerMode, caps.thermostatHeatingSetpoint, caps.airConditionerFanMode, caps.thermostatMode, - caps.thermostatOperatingState + caps.thermostatOperatingState, + caps.windowShade, + caps.windowShadeLevel }, capability_handlers = { -- Switch command handler @@ -30,6 +34,14 @@ Driver( [caps.switch.commands.on.NAME] = commands.set_switch, [caps.switch.commands.off.NAME] = commands.set_switch }, + -- Switch Level command handler + [caps.switchLevel.ID] = { + [caps.switchLevel.commands.setLevel.NAME] = commands.set_level + }, + -- Color Control command handler + [caps.colorTemperature.ID] = { + [caps.colorTemperature.commands.setColorTemperature.NAME] = commands.set_color + }, -- Refresh command handler [caps.refresh.ID] = { [caps.refresh.commands.refresh.NAME] = commands.refresh @@ -45,6 +57,16 @@ Driver( }, [caps.thermostatMode.ID] = { [caps.thermostatMode.commands.setThermostatMode.NAME] = commands.set_thermostat_mode + }, + -- Window Shade command handler + [caps.windowShade.ID] = { + [caps.windowShade.commands.open.NAME] = commands.set_shade, + [caps.windowShade.commands.close.NAME] = commands.set_shade, + [caps.windowShade.commands.pause.NAME] = commands.set_shade + }, + -- Window Shade Level command handler + [caps.windowShadeLevel.ID] = { + [caps.windowShadeLevel.commands.setShadeLevel.NAME] = commands.set_shade_level } } } @@ -60,6 +82,23 @@ function driver:set_switch(device, on_off) return device:emit_event(caps.switch.switch.on()) end +-- Switch level control for external commands +function driver:set_level(device, lvl) + device:online() + if lvl == 0 then + device:emit_event(caps.switch.switch.off()) + else + device:emit_event(caps.switch.switch.on()) + end + return device:emit_event(caps.switchLevel.level(lvl)) +end + +function driver:set_hue(device, hue) + device:online() + return device:emit_event(caps.colorTemperature.colorTemperature(hue*29999//255+1)) +end + + -- Switch control for external commands function driver:ac_report(device, onoff, mode, fan, settemp, temp, err) device:online() @@ -117,6 +156,31 @@ function driver:heater_report(device, onoff, settemp, temp, err) return true,nil end +-- Curtain control from bridge +function driver:curtain_report(device, onoff, level, err) + device:online() + if (onoff ~= nil) then + log.info('device '..device.parent_assigned_child_key..' report onoff '..onoff) + if (onoff == "close") then + device:emit_event(caps.windowShade.windowShade.closed()) + elseif (onoff == "open") then + device:emit_event(caps.windowShade.windowShade.open()) + else + device:emit_event(caps.windowShade.windowShade.paused()) + end + end + if (level ~= nil) then + log.info('device '..device.parent_assigned_child_key..' report level '..level) + 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 + end + return true,nil +end + -- global params init wisers.driver = driver -- start ssdp schedule to check bridge ip diff --git a/drivers/DeepSmart/deepsmart/src/lifecycles.lua b/drivers/DeepSmart/deepsmart/src/lifecycles.lua index 366e40037a..beaffede27 100644 --- a/drivers/DeepSmart/deepsmart/src/lifecycles.lua +++ b/drivers/DeepSmart/deepsmart/src/lifecycles.lua @@ -32,6 +32,8 @@ function lifecycle_handler.init(driver, device) elseif devtype == config.ENUM.HEATER then local supported_thermostat_modes = {'heat', 'off'} device:emit_event(caps.thermostatMode.supportedThermostatModes(supported_thermostat_modes, { visibility = { displayed = false } })) + elseif devtype == config.ENUM.CURTAIN then + wisers.default_report(driver, device) end ------------------- log.info('device '..device.parent_assigned_child_key..' in lifecycle init')