├── README.md ├── aeotec-multisensor ├── README.md ├── config.yml ├── fingerprints.yml ├── profiles │ └── aeotec-multisensor-profile.yml └── src │ ├── init.lua │ └── preferences.lua ├── custom-capability ├── README.md ├── capabilities │ ├── fancySwitch.presentation.yaml │ └── fancySwitch.yaml ├── config.yml ├── fingerprints.yml ├── presentation │ └── fancy-switch.presentation.yaml ├── profiles │ └── fancy-switch.yml └── src │ ├── init.lua │ └── test │ └── test_fancy_switch.lua ├── hello-world ├── README.md ├── config.yml ├── profiles │ └── hello-world.v1.yaml └── src │ ├── command_handlers.lua │ ├── discovery.lua │ └── init.lua ├── lightbulb-lan-esp8266 ├── LICENSE ├── README.md ├── app │ ├── README.md │ ├── doc │ │ └── esp8266-rgb-schematic.png │ └── src │ │ ├── config.lua │ │ ├── device_control.lua │ │ ├── init.lua │ │ ├── responses.lua │ │ ├── server.lua │ │ └── upnp.lua └── driver │ ├── config.yml │ ├── profiles │ └── LightBulb.yml │ └── src │ ├── commands.lua │ ├── config.lua │ ├── discovery.lua │ ├── init.lua │ ├── lifecycles.lua │ └── server.lua ├── st-multipurpose-sensor ├── README.md ├── config.yml ├── fingerprints.yml ├── profiles │ └── multipurpose-profile.yml └── src │ ├── common.lua │ ├── init.lua │ ├── smartthings │ └── init.lua │ └── test │ ├── test_smartthings_sensor.lua │ └── test_threeAxis_sensor.lua └── thingsim ├── README.md └── rpcclient ├── config.yml ├── profiles └── onoff.yaml └── src ├── discovery.lua ├── init.lua └── rpcclient.lua /README.md: -------------------------------------------------------------------------------- 1 | # SmartThings Edge Drivers 2 | 3 | ## What is SmartThings Edge? 4 | 5 | SmartThings Edge is our new architecture for Hub Connected devices that uses Device Drivers to execute commands locally on SmartThings Hubs. Edge Drivers are [Lua©-based](https://www.lua.org/) and can be used for Hub Connected devices, including Zigbee, Z-Wave, and LAN protocols. SmartThings Edge will bring new benefits such as reduced latency and cloud costs. 6 | 7 | ## Getting Started 8 | 9 | In this repository, you’ll find different ready-to-install sample Edge Drivers that can help you integrate your devices to the SmartThings platform. 10 | For more information on building Edge Drivers, look at the resources in [More Information](#More-Information) 11 | 12 | ## Zigbee SmartThings Multipurpose sensor 13 | 14 | Example integrating a SmartThings Multipurpose sensor, which has the following Capabilities: 15 | 16 | - Contact Sensor 17 | - Temperature Measurement 18 | - Acceleration/Vibration Sensor 19 | - Three Axis Sensor 20 | 21 | [Sample Code](./st-multipurpose-sensor) 22 | 23 | ## LAN RGB Light Bulb 24 | 25 | Example integrating an ESP8266 board via LAN. This device is configured to work as a RGB Light Bulb and has the following Capabilities: 26 | 27 | - Switch 28 | - Switch Level 29 | - Color Control 30 | 31 | [Sample Code](./lightbulb-lan-esp8266) 32 | 33 | ## Z-Wave Aeotec MultiSensor 6 34 | 35 | Example integrating the Aeotec’s MultiSensor 6 which has the following Capabilities: 36 | 37 | - Motion Sensor 38 | - Illuminance Measurement 39 | - Temperature Measurement 40 | 41 | [Sample Code](./aeotec-multisensor) 42 | 43 | ## `thingsim` device simulator 44 | 45 | Example **LAN device integration** through an **RPC Server** supporting the **Switch** capability. 46 | 47 | [Sample Code](./thingsim) 48 | 49 | ## Custom Capability Integration 50 | 51 | **Zigbee Driver** example that implements a **stock** and a **custom** capabilities. 52 | 53 | - Refresh 54 | - ``.fancySwitch 55 | 56 | [Sample Code](./custom-capability) 57 | 58 | ## Hello World example 59 | 60 | Example Driver to get started with **LAN-based device integrations**. This implementation uses the **Switch** capability. 61 | 62 | [Sample Code](./hello-world) 63 | 64 | ## Installation Tutorial 65 | 66 | Make sure you have the following: 67 | 68 | 1. The latest version of the SmartThings app ([Android](https://play.google.com/store/apps/details?id=com.samsung.android.oneconnect) | [iOS](https://apps.apple.com/us/app/smartthings/id1222822904)) 69 | 2. A SmartThings Hub with firmware version 38.x or greater 70 | 3. A compatible device ready to be integrated: 71 | 72 | a. Battery's level is enough for the device functionality (Zigbee Multi Sensor and Aeotec MultiSensor 6) 73 | 74 | b. The device was previously excluded from the Z-Wave network or is a fresh installation (Aeotec MultiSensor 6) 75 | 76 | c. You've installed the [LightBulb App](https://github.com/SmartThingsDevelopers/DeviceDrivers/tree/main/lightbulb-lan-esp8266/app) in the ESP8266 NodeMCU board and it's wired according to the [schematics](https://github.com/SmartThingsDevelopers/DeviceDrivers/tree/main/lightbulb-lan-esp8266/app#schematics) (LAN Lightbulb) 77 | 78 | You'll find further installation instructions in each sample and in the Tutorial Community posts: 79 | 80 | - [Tutorial | Creating Drivers for Zigbee Devices with SmartThings Edge](https://community.smartthings.com/t/creating-drivers-for-zigbee-devices-with-smartthings-edge/229502) 81 | - [Tutorial | Creating Drivers for LAN Devices with SmartThings Edge](https://community.smartthings.com/t/creating-drivers-for-lan-devices-with-smartthings-edge/229501) 82 | - [Tutorial | Creating Drivers for ZWave Devices with SmartThings Edge](https://community.smartthings.com/t/creating-drivers-for-zwave-devices-with-smartthings-edge/229503) 83 | - [Tutorial | Writing an RPC Client Edge Device Driver](https://community.smartthings.com/t/tutorial-writing-an-rpc-client-edge-device-driver/230285) 84 | 85 | ## More Information 86 | 87 | Take a look at the announcement of [SmartThings Edge](https://community.smartthings.com/t/announcing-smartthings-edge/229555) in our Community. 88 | 89 | ## Support 90 | 91 | If you have any questions about the specification document, visit [community.smartthings.com](https://community.smartthings.com/c/developer-programs). 92 | -------------------------------------------------------------------------------- /aeotec-multisensor/README.md: -------------------------------------------------------------------------------- 1 | # Sample Edge Driver for Aeotec MultiSensor 6 2 | 3 | Model: ZW100-(A, B, C and G) 4 | 5 | Protocol: Z-Wave 6 | 7 | ## Prerequisites 8 | 9 | 1. Set up the SmartThings CLI according to the [configuration document](https://github.com/SmartThingsCommunity/smartthings-cli/blob/master/packages/cli/doc/configuration.md). 10 | 2. Add the [Edge Driver plugin](https://github.com/SmartThingsCommunity/edge-alpha-cli-plugin#set-up) to the CLI. 11 | 3. Configure your development environment for the [SmartThingsEdgeDrivers](https://github.com/SmartThingsCommunity/SmartThingsEdgeDriversBeta) 12 | 4. A SmartThings hub with firmware version 000.038.000XX or greater and an Aeotec MultiSensor 6. 13 | 14 | ## Uploading Your Driver to SmartThings 15 | 16 | _Note: Take a look at the installation tutorial in our [Developer's Community](https://community.smartthings.com/t/creating-drivers-for-zwave-devices-with-smartthings-edge/229503)._ 17 | 18 | 1. Compile the driver: 19 | 20 | ``` 21 | smartthings edge:drivers:package driver/ 22 | ``` 23 | 24 | 2. Next, create a channel for your driver 25 | 26 | ``` 27 | smartthings edge:channels:create 28 | ``` 29 | 30 | 3. Enroll your driver into the channel 31 | 32 | ``` 33 | smartthings edge:channels:enroll 34 | ``` 35 | 36 | 4. Publish your driver to the channel 37 | 38 | ``` 39 | smartthings edge:drivers:publish 40 | ``` 41 | 42 | 5. If the package was successfully created, you can call the command below and follow the on-screen prompts to install the Driver in your Hub: 43 | 44 | ``` 45 | smartthings edge:drivers:install 46 | ``` 47 | 48 | You should see the confirmation message: "Driver {driver-id} installed to Hub {hub-id}" 49 | 50 | 6. Use your WiFi router or the [SmartThings IDE](https://account.smartthings.com/login) > My Hubs to locate and copy the IP Address for your Hub. 51 | 52 | 7. From a computer on the same local network as your Hub, open a new terminal window and run the command to get the logs from all the installed drivers. 53 | 54 | ``` 55 | smartthings edge:drivers:logcat --hub-address=x.x.x.x -a 56 | ``` 57 | 58 | ## Onboarding your New Device 59 | 60 | 1. Open the SmartThings App and go to the location where the hub is installed. 61 | 2. Go to Add (+) > Device or select _Scan Nearby_ (If you have more than one, select the corresponding Hub as well) 62 | 63 | 3. Put your device in pairing mode; the specifications will vary by manufacturer (for the [Aeotec MultiSensor 6](https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2684/Aeon%20Labs%20MultiSensor%206%20manual.pdf), press the device’s action button once). 64 | 65 | 4. Keep the terminal view open until the "infoChanged" lifecycle event is received and the driver starts getting the `REPORT` commands from the device. 66 | 67 | Example Output 68 | 69 | ```text 70 | received Z-Wave command: {args={alarm_level=0, alarm_type=0, event="MOTION_DETECTION", event_parameter="", notification_status="ON", notification_type="HOME_SECURITY", v1_alarm_level=0, v1_alarm_type=0, z_wave_alarm_event=8, z_wave_alarm_status="ON", z_wave_alarm_type="BURGLAR", zensor_net_source_node_id=0}, cmd_class="NOTIFICATION", cmd_id="REPORT", dst_channels={}, encap="S2_AUTH", payload="\x00\x00\x00\xFF\x07\x08\x00", src_channel=0, version=3} 71 | ``` 72 | 73 | If your Device paired correctly and the Driver was applied, you should not see any errors in the logs. You can validate this by opening the SmartThings app and controlling and/or viewing all of the devices Capabilities (e.g., motion or change the temperature). 74 | 75 | ## Additional Notes 76 | 77 | 1. This driver allows you to change 2 configuration parameters of the device. 78 | 2. When experimenting with Z-Wave devices, remember to exclude the device before re-pairing. 79 | -------------------------------------------------------------------------------- /aeotec-multisensor/config.yml: -------------------------------------------------------------------------------- 1 | name: 'Z-Wave Aeotec Multisensor' 2 | packageKey: 'zwave-aeotec-multisensor' 3 | permissions: 4 | zwave: {} -------------------------------------------------------------------------------- /aeotec-multisensor/fingerprints.yml: -------------------------------------------------------------------------------- 1 | zwaveManufacturer: 2 | - id: "Aeotec/ZW100-X" 3 | deviceLabel: Aeotec MultiSensor 6 4 | manufacturerId: 0x0086 5 | productId: 0x0064 6 | deviceProfileName: aeotec-multisensor-pr -------------------------------------------------------------------------------- /aeotec-multisensor/profiles/aeotec-multisensor-profile.yml: -------------------------------------------------------------------------------- 1 | name: aeotec-multisensor-pr 2 | components: 3 | - id: main 4 | capabilities: 5 | - id: motionSensor 6 | version: 1 7 | - id: illuminanceMeasurement 8 | version: 1 9 | - id: temperatureMeasurement 10 | version: 1 11 | - id: battery 12 | version: 1 13 | - id: healthCheck 14 | version: 1 15 | categories: 16 | - name: MotionSensor 17 | metadata: 18 | deviceType: MotionSensor 19 | ocfDeviceType: x.com.st.d.sensor.motion 20 | deviceTypeId: MotionSensor 21 | preferences: 22 | - name: "motionClearTime" 23 | title: "Set Motion Clear Time" 24 | description: "PIR time in seconds (default 240)" 25 | required: false 26 | preferenceType: integer 27 | definition: 28 | minimum: 10 29 | maximum: 3600 30 | default: 240 31 | - name: "motionSensorSensitivity" 32 | title: "Set Motion sensor sensitivity" 33 | description: "0-5 where 0 disables the sensor (default 5)" 34 | required: false 35 | preferenceType: integer 36 | definition: 37 | minimum: 0 38 | maximum: 5 39 | default: 5 -------------------------------------------------------------------------------- /aeotec-multisensor/src/init.lua: -------------------------------------------------------------------------------- 1 | local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 1 }) 2 | local capabilities = require "st.capabilities" 3 | local ZwaveDriver = require "st.zwave.driver" 4 | local defaults = require "st.zwave.defaults" 5 | local cc = require "st.zwave.CommandClass" 6 | local preferencesMap = require "preferences" 7 | local log = require "log" 8 | 9 | 10 | local ZWAVE_MOTION_TEMP_LIGHT_SENSOR_FINGERPRINTS = { 11 | {mfr = 0x0086, prod = 0x0002, model = 0x0064}, -- ZW100-C EU Aeotec MultiSensor 12 | {mfr = 0x0086, prod = 0x0102, model = 0x0064}, -- ZW100-A US Aeotec MultiSensor 13 | {mfr = 0x0086, prod = 0x0202, model = 0x0064}, -- ZW100-B AU Aeotec MultiSensor 14 | {mfr = 0x0086, prod = 0x0A02, model = 0x0064} -- ZW100-G JP Aeotec MultiSensor 15 | } 16 | 17 | local function can_handle_zwave_motion_temp_light_sensor(opts, driver, device, ...) 18 | for _, fingerprint in ipairs(ZWAVE_MOTION_TEMP_LIGHT_SENSOR_FINGERPRINTS) do 19 | if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then 20 | return true 21 | end 22 | end 23 | return false 24 | end 25 | 26 | local function parameterNumberToParameterName(preferences,parameterNumber) 27 | for id, parameter in pairs(preferences) do 28 | if parameter.parameter_number == parameterNumber then 29 | return id 30 | end 31 | end 32 | end 33 | 34 | --Update when a WakeUp notification is received 35 | local function update_preferences(self, device, args) 36 | for id, value in pairs(device.preferences) do 37 | local oldPreferenceValue = args.old_st_store.preferences[id] 38 | local newParameterValue = tonumber(device.preferences[id]) 39 | local syncValue = device:get_field(id) 40 | if preferencesMap[id] and (oldPreferenceValue ~= newParameterValue or syncValue == false) then 41 | device:send(Configuration:Set({parameter_number = preferencesMap[id].parameter_number, size = preferencesMap[id].size, configuration_value = newParameterValue})) 42 | device:set_field(id, false, {persist = true}) 43 | device:send(Configuration:Get({parameter_number = preferencesMap[id].parameter_number})) 44 | end 45 | end 46 | end 47 | 48 | --Verify if the preference where set in the device 49 | local function configuration_report(driver, device, cmd) 50 | if preferencesMap then 51 | local parameterName = parameterNumberToParameterName(preferencesMap, cmd.args.parameter_number) 52 | local configValueSetByUser = device.preferences[parameterName] 53 | local configValueReportedByDevice = cmd.args.configuration_value 54 | if (parameterName and configValueSetByUser == configValueReportedByDevice) then 55 | device:set_field(parameterName, true, {persist = true}) 56 | end 57 | end 58 | end 59 | 60 | local function init_dev(self, device) 61 | if preferencesMap then 62 | device:set_update_preferences_fn(update_preferences) 63 | for id, _ in pairs(preferencesMap) do 64 | device:set_field(id, true, {persist = true}) 65 | end 66 | end 67 | end 68 | 69 | local driver_template = { 70 | zwave_handlers = { 71 | [cc.CONFIGURATION] = { 72 | [Configuration.REPORT] = configuration_report 73 | } 74 | }, 75 | supported_capabilities = { 76 | capabilities.motionSensor, 77 | capabilities.temperatureMeasurement, 78 | capabilities.illuminanceMeasurement, 79 | capabilities.battery, 80 | }, 81 | lifecycle_handlers = { 82 | init = init_dev 83 | }, 84 | NAME = "zwave aeotec multisensor", 85 | can_handle = can_handle_zwave_motion_temp_light_sensor, 86 | } 87 | 88 | --[[ 89 | The default handlers take care of the Command Classes and the translation to capability events 90 | for most devices, but you can still define custom handlers to override them. 91 | ]]-- 92 | 93 | defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) 94 | local multiSensor = ZwaveDriver("zwave-aeotec-multisensor", driver_template) 95 | multiSensor:run() -------------------------------------------------------------------------------- /aeotec-multisensor/src/preferences.lua: -------------------------------------------------------------------------------- 1 | local preferences = { 2 | motionSensorSensitivity = {parameter_number = 4, size = 1}, 3 | motionClearTime = {parameter_number = 3, size = 2} 4 | } 5 | 6 | return preferences -------------------------------------------------------------------------------- /custom-capability/README.md: -------------------------------------------------------------------------------- 1 | Custom Capability Example 2 | ========================= 3 | 4 | This shows an example using a custom capability from within a driver. To get this 5 | working you can follow the information found in the community post and linked docs 6 | [here](https://community.smartthings.com/t/custom-capability-and-cli-developer-preview/197296). 7 | Once you have created a custom capability (in this example we simply created `fancySwitch` 8 | which is essentially the `switch` capability), you can then view the definition of your capability 9 | back as JSON using the CLI command. Note that for presentation purposes you are no longer using the `--dth` 10 | flag, and instead you should use the profile ID instead of the dth ID. 11 | 12 | ```shell script 13 | smartthings capabilities [ID] [VERSION] -o=cap.json 14 | ``` 15 | 16 | Once you have the definition in your package you can refer to the capability from the capabilities library. 17 | 18 | ```lua 19 | local capabilities = require "st.capabilities" 20 | local fancySwitch = capabilities["your_namespace.fancySwitch"] 21 | ``` 22 | 23 | It's important to note here that the syntax `capabilities.your_namespace.fancySwitch` is NOT supported. The 24 | combined `your_namespace.fancySwitch` is treated as a singular ID and thus the capabilities table needs 25 | to be indexed by the complete ID. 26 | 27 | If you want to register handlers for your capability commands you will have to place an entry in the 28 | capabilities table under the qualified name, so that when the command is received, the driver library 29 | code will be able to properly find and match the capability information. 30 | 31 | ```lua 32 | 33 | ... 34 | 35 | local driver_template = { 36 | capability_handlers = { 37 | [fancySwitch.ID] = { 38 | [fancySwitch.commands.fancyOn.NAME] = switch_defaults.on, 39 | [fancySwitch.commands.fancyOff.NAME] = switch_defaults.off, 40 | [fancySwitch.commands.fancySet.NAME] = fancy_set_handler, 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | At this point you should be able to use the capability like any other standard capability. 47 | 48 | Testing 49 | ------- 50 | 51 | Because the definition of your capability will be synced from the cloud when your driver is running on a hub, you 52 | will need to add a local definition that the libraries will be able to access in order for the integration tests to 53 | work as expected. The simplest way to do this is to navigate into your `lua_libs` directory and find 54 | `lua_libs/st/capabilities/generated` then within that directory create a new folder with the name of your 55 | namespace. Then within that directory create a lua file with a filename of `yourCapabilityId.lua` where the capability 56 | ID does NOT include your namespace. Then you can add to that file a single return statement and return a string with the 57 | JSON definition of your capability. 58 | 59 | ```lua 60 | return [[ 61 | { 62 | "id": "your_namespace.fancySwitch", 63 | "version": 1, 64 | ... 65 | } 66 | ]] 67 | ``` 68 | 69 | Once that is there, your tests should be able to refer to the capability as it would when running on the hub. 70 | 71 | This process will be improved in future updates to avoid the need to add to the library files. -------------------------------------------------------------------------------- /custom-capability/capabilities/fancySwitch.presentation.yaml: -------------------------------------------------------------------------------- 1 | dashboard: 2 | states: 3 | - label: '{{fancySwitch.value}}' 4 | alternatives: 5 | - key: 'On' 6 | value: Fancy on 7 | - key: 'Off' 8 | type: inactive 9 | value: Fancy off 10 | actions: 11 | - displayType: toggleSwitch 12 | toggleSwitch: 13 | command: 14 | 'on': fancyOn 15 | 'off': fancyOff 16 | state: 17 | value: fancySwitch.value 18 | 'on': 'On' 19 | 'off': 'Off' 20 | basicPlus: [] 21 | detailView: 22 | - label: Fancy Switch 23 | displayType: toggleSwitch 24 | toggleSwitch: 25 | command: 26 | 'on': fancyOn 27 | 'off': fancyOff 28 | state: 29 | value: fancySwitch.value 30 | 'on': 'On' 31 | 'off': 'Off' 32 | label: '{{fancySwitch.value}}' 33 | alternatives: 34 | - key: 'On' 35 | value: Fancy on 36 | - key: 'Off' 37 | type: inactive 38 | value: Fancy off 39 | id: your_namespace.fancySwitch 40 | version: 1 41 | -------------------------------------------------------------------------------- /custom-capability/capabilities/fancySwitch.yaml: -------------------------------------------------------------------------------- 1 | name: Fancy Switch 2 | attributes: 3 | fancySwitch: 4 | schema: 5 | type: object 6 | properties: 7 | value: 8 | type: string 9 | enum: 10 | - 'On' 11 | - 'Off' 12 | additionalProperties: false 13 | required: 14 | - value 15 | enumCommands: [] 16 | commands: 17 | fancyOff: 18 | name: fancyOff 19 | arguments: [] 20 | fancyOn: 21 | name: fancyOn 22 | arguments: [] 23 | fancySet: 24 | name: fancySet 25 | arguments: 26 | - name: state 27 | optional: false 28 | schema: 29 | type: string 30 | enum: 31 | - 'On' 32 | - 'Off' 33 | id: your_namespace.fancySwitch 34 | version: 1 35 | -------------------------------------------------------------------------------- /custom-capability/config.yml: -------------------------------------------------------------------------------- 1 | name: 'Zigbee Custom Switch' 2 | packageKey: 'zigbee-custom-switch' 3 | permissions: 4 | zigbee: {} 5 | -------------------------------------------------------------------------------- /custom-capability/fingerprints.yml: -------------------------------------------------------------------------------- 1 | zigbeeGeneric: 2 | - id: "dimmer-generic" 3 | deviceLabel: "Zigbee Fancy Swtich" 4 | clusters: 5 | server: 6 | - 0x0006 7 | deviceProfileName: fancy-switch 8 | -------------------------------------------------------------------------------- /custom-capability/presentation/fancy-switch.presentation.yaml: -------------------------------------------------------------------------------- 1 | mnmn: SmartThingsCommunity 2 | vid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 3 | version: 0.0.1 4 | type: profile 5 | dashboard: 6 | states: 7 | - component: main 8 | capability: your_namespace.fancySwitch 9 | version: 1 10 | idx: 0 11 | group: main 12 | values: [] 13 | actions: 14 | - component: main 15 | capability: your_namespace.fancySwitch 16 | version: 1 17 | idx: 0 18 | group: main 19 | detailView: 20 | - component: main 21 | capability: your_namespace.fancySwitch 22 | version: 1 23 | values: [] 24 | patch: [] 25 | - component: main 26 | capability: refresh 27 | version: 1 28 | values: [] 29 | patch: [] 30 | automation: 31 | conditions: 32 | - component: main 33 | capability: your_namespace.fancySwitch 34 | version: 1 35 | values: [] 36 | patch: [] 37 | exclusion: [] 38 | actions: 39 | - component: main 40 | capability: your_namespace.fancySwitch 41 | version: 1 42 | values: [] 43 | patch: [] 44 | exclusion: [] 45 | - component: main 46 | capability: refresh 47 | version: 1 48 | values: [] 49 | patch: [] 50 | exclusion: [] 51 | presentationId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 52 | manufacturerName: SmartThingsCommunity 53 | -------------------------------------------------------------------------------- /custom-capability/profiles/fancy-switch.yml: -------------------------------------------------------------------------------- 1 | name: fancy-switch 2 | components: 3 | - id: main 4 | capabilities: 5 | - id: "your_namespace.fancySwitch" 6 | version: 1 7 | - id: refresh 8 | version: 1 9 | categories: 10 | - name: Switch 11 | metadata: 12 | mnmn: SmartThingsCommunity 13 | vid: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 14 | -------------------------------------------------------------------------------- /custom-capability/src/init.lua: -------------------------------------------------------------------------------- 1 | local log = require "log" 2 | local capabilities = require "st.capabilities" 3 | local ZigbeeDriver = require "st.zigbee" 4 | local defaults = require "st.zigbee.defaults" 5 | local switch_defaults = require "st.zigbee.defaults.switch_defaults" 6 | local clusters = require "st.zigbee.zcl.clusters" 7 | local fancySwitch = capabilities["your_namespace.fancySwitch"] 8 | 9 | -- Capability command handlers 10 | 11 | local function fancy_on_handler(driver, device, command) 12 | log.error("fancy_on_handler") 13 | switch_defaults.on(driver, device, command) 14 | end 15 | 16 | local function fancy_off_handler(driver, device, command) 17 | log.error("fancy_off_handler") 18 | switch_defaults.off(driver, device, command) 19 | end 20 | 21 | local function fancy_set_handler(driver, device, command) 22 | log.error("fancy_set_handler") 23 | if command.args.state == "On" then 24 | switch_defaults.on(driver, device, command) 25 | elseif command.args.state == "Off" then 26 | switch_defaults.off(driver, device, command) 27 | end 28 | end 29 | 30 | -- Protocol handlers 31 | 32 | local function custom_on_off_attr_handler(driver, device, value, zb_rx) 33 | log.error("custom_on_off_attr_handler") 34 | device:emit_event(value.value and fancySwitch.fancySwitch.On() or fancySwitch.fancySwitch.Off()) 35 | end 36 | 37 | -- Lifecycle handlers 38 | 39 | local device_added = function(self, device) 40 | log.error("device_added") 41 | device:emit_event(fancySwitch.fancySwitch.On()) 42 | end 43 | 44 | local zigbee_fancy_switch_driver_template = { 45 | supported_capabilities = { 46 | fancySwitch, 47 | }, 48 | zigbee_handlers = { 49 | attr = { 50 | [clusters.OnOff.ID] = { 51 | [clusters.OnOff.attributes.OnOff.ID] = custom_on_off_attr_handler 52 | } 53 | } 54 | }, 55 | capability_handlers = { 56 | [fancySwitch.ID] = { 57 | [fancySwitch.commands.fancyOn.NAME] = fancy_on_handler, 58 | [fancySwitch.commands.fancyOff.NAME] = fancy_off_handler, 59 | [fancySwitch.commands.fancySet.NAME] = fancy_set_handler, 60 | } 61 | }, 62 | cluster_configurations = { 63 | { 64 | cluster = clusters.OnOff, 65 | attribute = clusters.OnOff.attributes.OnOff, 66 | minimum_interval = 0, 67 | maximum_interval = 300, 68 | } 69 | }, 70 | lifecycle_handlers = { 71 | added = device_added, 72 | } 73 | } 74 | 75 | defaults.register_for_default_handlers(zigbee_fancy_switch_driver_template, zigbee_fancy_switch_driver_template.supported_capabilities) 76 | local zigbee_fancy_switch = ZigbeeDriver("zigbee_fancy_switch", zigbee_fancy_switch_driver_template) 77 | zigbee_fancy_switch:run() 78 | -------------------------------------------------------------------------------- /custom-capability/src/test/test_fancy_switch.lua: -------------------------------------------------------------------------------- 1 | -- Mock out globals 2 | local test = require "integration_test" 3 | local clusters = require "st.zigbee.zcl.clusters" 4 | local OnOff = clusters.OnOff 5 | local Level = clusters.Level 6 | local capabilities = require "st.capabilities" 7 | local zigbee_test_utils = require "integration_test.zigbee_test_utils" 8 | local base64 = require "st.base64" 9 | local zigbee_constants = require "st.zigbee.constants" 10 | local fancySwitch = capabilities["your_namespace.fancySwitch"] 11 | local fancy_switch_profile = { 12 | components = { 13 | main = { 14 | capabilities = { 15 | [fancySwitch.ID] = { id = fancySwitch.ID }, 16 | }, 17 | id = "main" 18 | } 19 | } 20 | } 21 | 22 | local mock_simple_device = test.mock_device.build_test_zigbee_device({ profile = fancy_switch_profile }) 23 | 24 | zigbee_test_utils.prepare_zigbee_env_info() 25 | local function test_init() 26 | test.mock_device.add_test_device(mock_simple_device) 27 | zigbee_test_utils.init_noop_health_check_timer() 28 | end 29 | 30 | test.set_test_init_function(test_init) 31 | 32 | test.register_message_test( 33 | "Fancy on should be generated", 34 | { 35 | { 36 | channel = "zigbee", 37 | direction = "receive", 38 | message = { mock_simple_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_simple_device, 39 | true) } 40 | }, 41 | { 42 | channel = "capability", 43 | direction = "send", 44 | message = mock_simple_device:generate_test_message("main", fancySwitch.fancySwitch.On()) 45 | } 46 | } 47 | ) 48 | 49 | test.register_message_test( 50 | "Fancy off should be generated", 51 | { 52 | { 53 | channel = "zigbee", 54 | direction = "receive", 55 | message = { mock_simple_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_simple_device, 56 | false) } 57 | }, 58 | { 59 | channel = "capability", 60 | direction = "send", 61 | message = mock_simple_device:generate_test_message("main", fancySwitch.fancySwitch.Off()) 62 | } 63 | } 64 | ) 65 | 66 | test.register_message_test( 67 | "fancyOn should be handled", 68 | { 69 | { 70 | channel = "capability", 71 | direction = "receive", 72 | message = { mock_simple_device.id, { capability = "your_namespace.fancySwitch", component = "main", command = "fancyOn", args = { } } } 73 | }, 74 | { 75 | channel = "zigbee", 76 | direction = "send", 77 | message = { mock_simple_device.id, OnOff.server.commands.On(mock_simple_device) } 78 | } 79 | } 80 | ) 81 | 82 | test.register_message_test( 83 | "fancyOff should be handled", 84 | { 85 | { 86 | channel = "capability", 87 | direction = "receive", 88 | message = { mock_simple_device.id, { capability = "your_namespace.fancySwitch", component = "main", command = "fancyOff", args = { } } } 89 | }, 90 | { 91 | channel = "zigbee", 92 | direction = "send", 93 | message = { mock_simple_device.id, OnOff.server.commands.Off(mock_simple_device) } 94 | } 95 | } 96 | ) 97 | 98 | test.register_message_test( 99 | "fancySet true should be handled", 100 | { 101 | { 102 | channel = "capability", 103 | direction = "receive", 104 | message = { mock_simple_device.id, { capability = "your_namespace.fancySwitch", component = "main", command = "fancySet", args = { "On" } } } 105 | }, 106 | { 107 | channel = "zigbee", 108 | direction = "send", 109 | message = { mock_simple_device.id, OnOff.server.commands.On(mock_simple_device) } 110 | } 111 | } 112 | ) 113 | 114 | test.register_message_test( 115 | "fancySet false should be handled", 116 | { 117 | { 118 | channel = "capability", 119 | direction = "receive", 120 | message = { mock_simple_device.id, { capability = "your_namespace.fancySwitch", component = "main", command = "fancySet", args = { "Off" } } } 121 | }, 122 | { 123 | channel = "zigbee", 124 | direction = "send", 125 | message = { mock_simple_device.id, OnOff.server.commands.Off(mock_simple_device) } 126 | } 127 | } 128 | ) 129 | 130 | test.run_registered_tests() 131 | -------------------------------------------------------------------------------- /hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World Example 2 | 3 | This example is used in the [Get Started with SmartThings Edge](https://developer-preview.smartthings.com/docs/devices/hub-connected/get-started) 4 | guide. 5 | -------------------------------------------------------------------------------- /hello-world/config.yml: -------------------------------------------------------------------------------- 1 | name: 'Hello World' 2 | packageKey: 'com.smartthings.helloworld' 3 | permissions: 4 | discovery: {} 5 | -------------------------------------------------------------------------------- /hello-world/profiles/hello-world.v1.yaml: -------------------------------------------------------------------------------- 1 | # hello-world-profile 2 | name: hello-world.v1 3 | components: 4 | - id: main 5 | capabilities: 6 | - id: switch 7 | version: 1 8 | categories: 9 | - name: Light 10 | -------------------------------------------------------------------------------- /hello-world/src/command_handlers.lua: -------------------------------------------------------------------------------- 1 | local log = require "log" 2 | local capabilities = require "st.capabilities" 3 | 4 | local command_handlers = {} 5 | 6 | -- callback to handle an `on` capability command 7 | function command_handlers.switch_on(driver, device, command) 8 | log.debug(string.format("[%s] calling set_power(on)", device.device_network_id)) 9 | device:emit_event(capabilities.switch.switch.on()) 10 | end 11 | 12 | -- callback to handle an `off` capability command 13 | function command_handlers.switch_off(driver, device, command) 14 | log.debug(string.format("[%s] calling set_power(off)", device.device_network_id)) 15 | device:emit_event(capabilities.switch.switch.off()) 16 | end 17 | 18 | return command_handlers 19 | -------------------------------------------------------------------------------- /hello-world/src/discovery.lua: -------------------------------------------------------------------------------- 1 | local log = require "log" 2 | local discovery = {} 3 | 4 | -- handle discovery events, normally you'd try to discover devices on your 5 | -- network in a loop until calling `should_continue()` returns false. 6 | function discovery.handle_discovery(driver, _should_continue) 7 | log.info("Starting Hello World Discovery") 8 | 9 | local metadata = { 10 | type = "LAN", 11 | -- the DNI must be unique across your hub, using static ID here so that we 12 | -- only ever have a single instance of this "device" 13 | device_network_id = "hello thing", 14 | label = "Hello World Device", 15 | profile = "hello-world.v1", 16 | manufacturer = "SmartThings", 17 | model = "v1", 18 | vendor_provided_label = nil 19 | } 20 | 21 | -- tell the cloud to create a new device record, will get synced back down 22 | -- and `device_added` and `device_init` callbacks will be called 23 | driver:try_create_device(metadata) 24 | end 25 | 26 | return discovery 27 | -------------------------------------------------------------------------------- /hello-world/src/init.lua: -------------------------------------------------------------------------------- 1 | -- require st provided libraries 2 | local capabilities = require "st.capabilities" 3 | local Driver = require "st.driver" 4 | local log = require "log" 5 | 6 | -- require custom handlers from driver package 7 | local command_handlers = require "command_handlers" 8 | local discovery = require "discovery" 9 | 10 | ----------------------------------------------------------------- 11 | -- local functions 12 | ----------------------------------------------------------------- 13 | -- this is called once a device is added by the cloud and synchronized down to the hub 14 | local function device_added(driver, device) 15 | log.info("[" .. device.id .. "] Adding new Hello World device") 16 | 17 | -- set a default or queried state for each capability attribute 18 | device:emit_event(capabilities.switch.switch.on()) 19 | end 20 | 21 | -- this is called both when a device is added (but after `added`) and after a hub reboots. 22 | local function device_init(driver, device) 23 | log.info("[" .. device.id .. "] Initializing Hello World device") 24 | 25 | -- mark device as online so it can be controlled from the app 26 | device:online() 27 | end 28 | 29 | -- this is called when a device is removed by the cloud and synchronized down to the hub 30 | local function device_removed(driver, device) 31 | log.info("[" .. device.id .. "] Removing Hello World device") 32 | end 33 | 34 | -- create the driver object 35 | local hello_world_driver = Driver("helloworld", { 36 | discovery = discovery.handle_discovery, 37 | lifecycle_handlers = { 38 | added = device_added, 39 | init = device_init, 40 | removed = device_removed 41 | }, 42 | capability_handlers = { 43 | [capabilities.switch.ID] = { 44 | [capabilities.switch.commands.on.NAME] = command_handlers.switch_on, 45 | [capabilities.switch.commands.off.NAME] = command_handlers.switch_off, 46 | }, 47 | } 48 | }) 49 | 50 | -- run the driver 51 | hello_world_driver:run() 52 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SmartThingsCommunity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/README.md: -------------------------------------------------------------------------------- 1 | # Sample Edge Driver for ESP8266 Light bulb 2 | 3 | Model: NodeMCU ESP8266 4 | 5 | Protocol: LAN 6 | 7 | ## Prerequisites 8 | 9 | A SmartThings Hub with firmware version 38.x or greater and a LAN device ready to connect. 10 | 11 | For this tutorial, we used an ESP8266 but the same principles can be used to integrate any LAN-based device that supports SSDP and HTTP. 12 | 13 | 1. Set up the SmartThings CLI according to the [configuration document](https://github.com/SmartThingsCommunity/smartthings-cli/blob/master/packages/cli/doc/configuration.md). 14 | 2. Add the [Edge Driver plugin](https://github.com/SmartThingsCommunity/edge-alpha-cli-plugin#set-up) to the CLI. 15 | 3. Configure your development environment for the [SmartThingsEdgeDrivers](https://github.com/SmartThingsCommunity/SmartThingsEdgeDriversBeta) 16 | 17 | ## Uploading Your Driver to SmartThings 18 | 19 | _Note: Take a look at the installation tutorial in our [Developer's Community](https://community.smartthings.com/t/creating-drivers-for-zwave-devices-with-smartthings-edge/229503)._ 20 | 21 | 1. Compile the driver: 22 | 23 | ``` 24 | smartthings edge:drivers:package driver/ 25 | ``` 26 | 27 | 2. Create a channel for your driver 28 | 29 | ``` 30 | smartthings edge:channels:create 31 | ``` 32 | 33 | 3. Enroll your driver into the channel 34 | 35 | ``` 36 | smartthings edge:channels:enroll 37 | ``` 38 | 39 | 4. Publish your driver to the channel 40 | 41 | ``` 42 | smartthings edge:drivers:publish 43 | ``` 44 | 45 | 5. If the package was successfully created, you can call the command below and follow the on-screen prompts to install the Driver in your Hub: 46 | 47 | ``` 48 | smartthings edge:drivers:install 49 | ``` 50 | 51 | You should see the confirmation message: `Driver {driver-id} installed to Hub {hub-id}` 52 | 53 | 6. Use your WiFi router or the [SmartThings IDE](https://account.smartthings.com/login) > My Hubs to locate and copy the IP Address for your Hub. 54 | 55 | 7. From a computer on the same local network as your Hub, open a new terminal window and run the command to get the logs from all the installed drivers. 56 | 57 | ``` 58 | smartthings edge:drivers:logcat --hub-address=x.x.x.x -a 59 | ``` 60 | 61 | ## Onboarding your New Device 62 | 63 | 1. Setup the ESP8266 board and embedded app according to [these instructions](./app). 64 | 2. Open the _SmartThings App_ and follow these steps _(notice that you must add the device in the same location your Hub is installed)_: 65 | 66 | - Select **Add (+)** and then **Device**. 67 | - Tap on **Scan nearby** and check the logs emitted at your _logcat_ session. 68 | 69 | As soon as your device gets installed, the _Driver_ will send a 70 | periodic _Ping HTTP Requests_ with **IP and Port** reference of the server that will 71 | listen for external device updates at `X.X.X.X:XXXXX/push-state`. 72 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/README.md: -------------------------------------------------------------------------------- 1 | # ESP8266 LAN LightBulb 2 | 3 | ### Hardware Requirements 4 | 5 | 1. _ESP8266 NodeMCU_ board. 6 | 1. **Common annode** RGB LED. 7 | 1. Jumpers or copper wires. 8 | 9 | ### Schematics 10 | 11 | esp8266-schematic 12 | 13 | ## Prerequisites 14 | 15 | 1. Development Tools: 16 | 17 | - [esptool.py](https://github.com/espressif/esptool#esptoolpy) 18 | - [NodeMCU-Tool](https://github.com/AndiDittrich/NodeMCU-Tool#nodemcu-tool). 19 | - Linux `screen` command installed for serial communication. 20 | 21 | If you have issues with any of the above mentioned tools, you can refer to these alternatives: 22 | 23 | - For serial communication, you use either [picocom](https://github.com/npat-efault/picocom#picocom) or [minicom](https://github.com/Distrotech/minicom). 24 | - To embed the app you can use [ESPlorer](https://github.com/4refr0nt/ESPlorer). 25 | 26 | 1. Create at NodeMCU firmware at [NodeMCU Build](https://nodemcu-build.com/): 27 | 28 | - Specify an email where you'll get the firmware binaries. 29 | - It must point to the `release` branch of the [NodeMCU](https://github.com/nodemcu/nodemcu-firmware/) repository. 30 | - Whitelist the following modules: `file`, `HTTP`, `net`, `node`, `pwm2`, `sjson`, `timer`, and `wifi`. 31 | - Wait until you get the binaries via email and once you receive them, download the one that ends with `float.bin` into the `app/` directory. 32 | 33 | 1. Flash the firmware and test it. 34 | 35 | - Set permissions over the `/dev/ttyUSB0` port and register your linux user into the `dialog` group. This will allow you to use the `nodemcu-tool` properly _(if it fails, reboot your computer to refresh the system changes)_: 36 | 37 | sudo chmod -R 777 /dev/ttyUSB0 38 | sudo usermod -a -G dialout 39 | 40 | # if you whish to remove the user from 'dialout' group 41 | sudo gpasswd -d dialout 42 | 43 | - Erase flash: 44 | 45 | esptool.py --port /dev/ttyUSB0 erase_flash 46 | 47 | - Flash NodeMCU firmware: 48 | 49 | esptool.py --port /dev/ttyUSB0 \ 50 | write_flash 0x00000 \ 51 | app/nodemcu-release-9-modules-XXXX-XX-XX-XX-XX-XX-float.bin 52 | 53 | - Open the _NodeMCU shell_: 54 | 55 | sudo screen /dev/ttyUSB0 115200 56 | 57 | - Reboot your _ESP8266_ and you should see a similar output as the following: 58 | 59 | ��l����n�|r��N|�l�$`c����|{��$�n��N�l`��r�l�l�l ��s�p��$�$ ��r�d���$l 60 | NodeMCU 3.0.0.0 built on nodemcu-build.com provided by frightanic.com 61 | branch: release 62 | commit: d4ae3c364bd8ae3ded8b77d35745b7f07879f5f9 63 | release: 64 | release DTS: 202105102018 65 | SSL: false 66 | build type: float 67 | LFS: 0x0 bytes total capacity 68 | modules: file,http,net,node,pwm2,tmr,wifi 69 | build 2021-05-26 18:11 powered by Lua 5.1.4 on SDK 3.0.1-dev(fce080e) 70 | > 71 | 72 | 1. To upload the app into the device, exit the `screen` session _(`Ctrl + A, K, Y`)_ and then run the following command: 73 | 74 | nodemcu-tool upload app/src/* 75 | 76 | 1. Open the _NodeMCU shell_ to monitor logs: 77 | 78 | sudo screen /dev/ttyUSB 115200 79 | 80 | ### Authentication 81 | 82 | To **avoid hardcoded Wi-Fi credentials**, you'll need to connect to your _ESP8266's 83 | Access Point_ following these steps: 84 | 85 | 1. Go to your phone's _Network Settings_ and locate the **LightBulb-ESP8266** network. 86 | 2. One you're connected successfully, open your phone's browser and go to 87 | `192.168.4.1` to 88 | set up the credentials of your device's network _(refer to [./src/config.lua](./src/config.lua))_. 89 | 3. At the `screen` session, you should see some logs as soon as your device gets 90 | authenticated at the access point. 91 | 4. Switch back to your main network, to stay **synced with your Hub** and proceed to 92 | [install your driver](../driver/README.md). 93 | 94 | ### Built-In Controller 95 | 96 | To test if your device has been connected properly, you can access the device's 97 | built-in controller at `192.168.X.XX/control`. Notice that the IP will depend on 98 | your phone's current network, _i.e._ if you're connected to the _LightBulb-ESP8266_ 99 | network, the _controller_ will be at `192.168.4.1/control`. 100 | 101 | Also, I recommend you to test this feature after the device has been integrated into 102 | the _SmartThings ecosystem_, this way you'll be able to see bidirectional communication. 103 | 104 | --- 105 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/doc/esp8266-rgb-schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartThingsDevelopers/SampleDrivers/221edef9e0a8c2072778a6aa5cb70cd26f16609d/lightbulb-lan-esp8266/app/doc/esp8266-rgb-schematic.png -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/config.lua: -------------------------------------------------------------------------------- 1 | ---------------- 2 | -- Socket Config 3 | -- UDP UPnP 4 | MC_PORT=1900 5 | LOCAL_ADDR='0.0.0.0' 6 | MC_ADDR='239.255.255.250' 7 | -- TCP Server 8 | SRV_PORT=80 9 | 10 | -------------------- 11 | -- Wifi Access Point 12 | -- config 13 | WIFI_AP_CONFIG = { 14 | ssid='LightBulb-ESP8266', 15 | pwd='dummy-passphrase', 16 | save=true, 17 | hidden=false, 18 | max=1 19 | } 20 | 21 | -------------- 22 | -- Device info 23 | DEV = { 24 | CHIP_ID=string.format('%x', node.chipid()), 25 | SN='SN-ESP8266-696', 26 | MN='SmartThingsCommunity', 27 | NAME='LightBulbESP8266', 28 | TYPE='LAN', 29 | ext_uuid=nil, 30 | cache={ 31 | lvl=0, -- 0%/off 32 | clr={r=0,g=0,b=0} -- e.g. 255 each 33 | }, 34 | HUB = { addr=nil, port=nil } 35 | } 36 | 37 | -------------- 38 | -- GPIO config 39 | MAIN_GPIO=1 40 | RED_GPIO=5 41 | GREEN_GPIO=6 42 | BLUE_GPIO=7 43 | PWM_FREQ=800 44 | PULSE_PRD=255 45 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/device_control.lua: -------------------------------------------------------------------------------- 1 | --------------------- 2 | -- LED Switch - Level 3 | function led_lvl_ctl(lvl) 4 | lvl = (PULSE_PRD / 100) * lvl 5 | DEV.cache.lvl = lvl 6 | return pwm2.set_duty(MAIN_GPIO, math.floor(lvl)) 7 | end 8 | 9 | -------------------- 10 | -- LED Color Control 11 | function led_clr_ctl(r, g, b) 12 | DEV.cache.clr.r = r 13 | DEV.cache.clr.g = g 14 | DEV.cache.clr.b = b 15 | -- due to common anode rgb led, 16 | -- values will be opposite, 17 | -- e.g. 255 = off, 0 = on 18 | pwm2.set_duty(RED_GPIO, (PULSE_PRD - DEV.cache.clr.r)) 19 | pwm2.set_duty(GREEN_GPIO, (PULSE_PRD - DEV.cache.clr.g)) 20 | pwm2.set_duty(BLUE_GPIO, (PULSE_PRD - DEV.cache.clr.b)) 21 | return 22 | end 23 | 24 | --------------------- 25 | -- LED Switch Control 26 | function led_switch_ctl(on_off) 27 | if on_off == 'off' then 28 | return pwm2.set_duty(MAIN_GPIO, 0) 29 | end 30 | 31 | if DEV.cache.lvl == 0 then 32 | return pwm2.set_duty(MAIN_GPIO, 255) 33 | end 34 | return pwm2.set_duty( 35 | MAIN_GPIO, math.floor(DEV.cache.lvl)) 36 | end 37 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/init.lua: -------------------------------------------------------------------------------- 1 | ------------------------ 2 | -- Load config & modules 3 | dofile('config.lua') 4 | dofile('device_control.lua') 5 | dofile('server.lua') 6 | dofile('upnp.lua') 7 | 8 | --------------------------- 9 | -- Init Device Access Point 10 | print([[ 11 | > LightBulb ESP8266 App Ready... 12 | > WiFi Access Point enabled... 13 | ]]) 14 | 15 | wifi.setmode(wifi.STATIONAP) 16 | wifi.ap.config(WIFI_AP_CONFIG) 17 | ----------------------------- 18 | -- Init main network services 19 | function wifi_sta_start(ssid, pwd) -- credentials will be forgotten as soon as device reboots 20 | local wifi_config = { ssid=ssid, pwd=pwd, save=true } 21 | wifi.sta.config(wifi_config) 22 | print('connecting to wifi...') 23 | end 24 | 25 | ---------------------------- 26 | -- Init Switch and Switch 27 | -- Level gpio off by default 28 | pwm2.setup_pin_hz(MAIN_GPIO, PWM_FREQ, PULSE_PRD, DEV.cache.lvl) -- off 29 | -- Due to the Common anode RGB 30 | -- LED, duty is inverted 31 | pwm2.setup_pin_hz(GREEN_GPIO, PWM_FREQ, PULSE_PRD, DEV.cache.clr.g) 32 | pwm2.setup_pin_hz(RED_GPIO, PWM_FREQ, PULSE_PRD, DEV.cache.clr.r) 33 | pwm2.setup_pin_hz(BLUE_GPIO, PWM_FREQ, PULSE_PRD, DEV.cache.clr.b) 34 | pwm2.start() 35 | 36 | -------------- 37 | -- init server 38 | server_start() 39 | 40 | --------------------- 41 | -- Wifi event monitor 42 | -- callbacks: 43 | -- 44 | -- STATION Connected 45 | wifi.eventmon.register( 46 | wifi.eventmon.STA_CONNECTED, 47 | function(evt) 48 | print( 49 | 'service: station\r\n'.. 50 | 'status: connected\r\n'.. 51 | 'ssid: '..evt.SSID..'\r\n'.. 52 | 'bssid: '..evt.BSSID..'\r\n') 53 | end) 54 | 55 | -- STATION Disconnected 56 | wifi.eventmon.register( 57 | wifi.eventmon.STA_DISCONNECTED, 58 | function (evt) 59 | print( 60 | 'service: station\r\n'.. 61 | 'status: disconnected\r\n'.. 62 | 'reason: '..evt.reason..'\r\n'.. 63 | 'ssid: '..evt.SSID..'\r\n'.. 64 | 'bssid: '..evt.BSSID..'\r\n') 65 | end) 66 | 67 | -- STATION IP ready 68 | wifi.eventmon.register( 69 | wifi.eventmon.STA_GOT_IP, 70 | function (evt) 71 | print( 72 | 'service: station\r\n'.. 73 | 'status: IP Address ready\r\n'.. 74 | 'action: start UPnP Socket\r\n'.. 75 | 'netmask: '..evt.netmask..'\r\n'.. 76 | 'gateway: '..evt.netmask..'\r\n'.. 77 | '>>> DEVICE AVAILABLE OVER LAN AT: '..evt.IP..'\r\n') 78 | -- initialize ssdp session 79 | upnp_start() 80 | end) 81 | 82 | -- ACCESS POINT new client 83 | wifi.eventmon.register( 84 | wifi.eventmon.AP_STACONNECTED, 85 | function (evt) 86 | print( 87 | 'service: access point\r\n'.. 88 | 'action: start LAN AP socket\r\n'.. 89 | 'status: client connected\r\n'.. 90 | 'MAC: '..evt.MAC..'\r\n'.. 91 | 'AID: '..evt.AID..'\r\n') 92 | end) 93 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/responses.lua: -------------------------------------------------------------------------------- 1 | local res_handler = {} 2 | 3 | function res_handler.ok_200(body) 4 | -- Monkey patch on default header 5 | -- because the Driver socket.http/ltn12 6 | -- response handler fails to 7 | -- handle text/plain responses 8 | -- 9 | -- Update below when fixed: 10 | --local cnt_type = 'text/plain' 11 | local cnt_type = '' 12 | local status = 'HTTP/1.1 200 OK' 13 | local cache_stat = 'Cache-Control : no-cache, private' 14 | 15 | -- Handle NULL response body 16 | body = body or '' 17 | 18 | -- Handle Content-Type Header 19 | if type(body) == 'string' then 20 | -- HTML Response 21 | if body:find('html') then 22 | cnt_type = 'Content-Type: text/html' 23 | -- XML Response 24 | elseif body:find('xml') then 25 | cnt_type = 'Content-Type: text/xml' 26 | end 27 | -- JSON Response 28 | elseif type(body) == 'table' then 29 | cnt_type = 'Content-Type: application/json' 30 | body = sjson.encode(body) 31 | end 32 | 33 | -- Handle Content-Length Header 34 | local cnt_length = 'Content-Length: '..#body 35 | 36 | -- Build response 37 | local res = {} 38 | table.insert(res, status) 39 | table.insert(res, cache_stat) 40 | table.insert(res, cnt_length) 41 | table.insert(res, cnt_type) 42 | 43 | res = {table.concat(res, '\r\n'), body} 44 | return table.concat(res, '\r\n\r\n') 45 | end 46 | 47 | -- HTML Views 48 | res_handler.WIFI_CONFIG_VIEW = 49 | [[ 50 | 51 | 53 | 54 | 55 |

Wifi setup

56 |

Configure your WiFi Router

57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 | 66 | ]] 67 | 68 | res_handler.REDIRECT_VIEW = 69 | [[ 70 | 71 | 73 | 74 | 75 |

Connecting...

76 |

You can switch back to your main network...

77 | 78 | ]] 79 | 80 | res_handler.CONTROL_VIEW= 81 | [[ 82 | 83 | 85 | 86 | 87 |

Device Control

88 |
89 |

Switch

90 | 91 | 92 |
93 |
94 |

Switch Level

95 | 96 |
97 | 119 | 120 | ]] 121 | 122 | res_handler.DEVICE_INFO_XML = 123 | table.concat({ 124 | "", 125 | "", 126 | "", 127 | "2", 128 | "0", 129 | "", 130 | "", 131 | "urn:SmartThingsCommunity:device:Light:1", 132 | "/", 133 | ""..DEV.NAME.."", 134 | ""..DEV.MN.."", 135 | "https://community.smartthings.com", 136 | "RGB LightBulb", 137 | ""..DEV.SN.."", 138 | "uuid:"..DEV.CHIP_ID.."-"..DEV.SN.."", 139 | "" 140 | }, '\r\n') 141 | 142 | return res_handler 143 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/server.lua: -------------------------------------------------------------------------------- 1 | local res = require('responses') 2 | 3 | ---------------------- 4 | -- Query String Parser 5 | local function qsparse(str) 6 | local res = {} 7 | for k,v in str:gmatch('([%w-_]+)=([%w+-_!@#$%^]+)') do 8 | res[k:lower()] = v 9 | end 10 | return res 11 | end 12 | ----------------------- 13 | -- HTTP Response Parser 14 | local function httpparse(data) 15 | local res = {} 16 | res.status = data:sub(0, data:find('\r\n')) 17 | for k, v in data:gmatch('([%w-]+): ([%a+-: /=]+)') do 18 | res[k:lower()] = v 19 | end 20 | return res 21 | end 22 | 23 | -------------------- 24 | -- Push Remote State 25 | local function push_state(data) 26 | if not DEV.HUB.addr or not DEV.HUB.port then 27 | print('NO HUB REGISTERED') 28 | return nil 29 | end 30 | 31 | -- Prepare URL 32 | local url = string.format( 33 | 'http://%s:%s/push-state', DEV.HUB.addr, DEV.HUB.port) 34 | -- JSONstringify table 35 | data.uuid = DEV.HUB.ext_uuid 36 | local data = sjson.encode(data) 37 | 38 | print('PUSH STATE\r\nURL: '..url.. 39 | '\r\nDATA: '..data) 40 | return http.post( 41 | url,'Content-Type: application/json\r\n',data, 42 | function(code, data) print(code, data) end) 43 | end 44 | 45 | ----------------- 46 | -- ESP8266 Server 47 | function server_start() 48 | -- Request receiver calback 49 | local function recv_cb(conn, data) 50 | print('INCOMING HTTP REQUEST:\r\n'..data) 51 | -- parse query string 52 | local httpdata = httpparse(data) 53 | local qs = qsparse(httpdata.status) 54 | 55 | -- Collect WiFi Configuration 56 | -- params to initialize Wifi 57 | -- Station service. 58 | if qs.ssid and qs.pwd then 59 | wifi_sta_start(qs.ssid, qs.pwd) 60 | conn:send( 61 | res.ok_200(res.REDIRECT_VIEW)) 62 | 63 | -- Resource that provides the 64 | -- metadata of the device. 65 | -- This resource is provided 66 | -- via ssdp response. 67 | elseif httpdata.status:find(DEV.NAME..'.xml') then 68 | return conn:send( 69 | res.ok_200(res.DEVICE_INFO_XML)) 70 | 71 | -- Resource that will allow to 72 | -- register a parent node (Hub) 73 | -- storing its address and port. 74 | elseif httpdata.status:find('/ping?') then 75 | if qs.ip and qs.port then 76 | DEV.HUB.addr = qs.ip 77 | DEV.HUB.port = qs.port 78 | DEV.HUB.ext_uuid = qs.ext_uuid 79 | print( 80 | '\r\nPING\r\n'.. 81 | 'HUB LOCATION: http://'.. 82 | qs.ip..':'..qs.port.. 83 | '\r\nEXT_UUID: '..qs.ext_uuid..'\r\n') 84 | return conn:send(res.ok_200()) 85 | end 86 | 87 | -- Resource that will allow 88 | -- device state poll retrieving 89 | -- the raw state at the DEV.cache 90 | -- table JSON formatted. 91 | elseif httpdata.status:find('/refresh') then 92 | return conn:send(res.ok_200(DEV.cache)) 93 | 94 | -- Resource that allows to 95 | -- unlink the registered 96 | -- parent node (Hub) 97 | elseif httpdata.status:find('/delete') then 98 | print('HUB REVOKED') 99 | DEV.HUB.addr = nil 100 | DEV.HUB.port = nil 101 | DEV.HUB.ext_uuid = nil 102 | return conn:send( 103 | res.ok_200()) 104 | 105 | -- Resource that allows device 106 | -- control either at the ST App 107 | -- or at browsers 108 | elseif httpdata.status:find('/control') then 109 | local push_data = nil 110 | -- Switch 111 | if qs.switch then 112 | led_switch_ctl(qs.switch) 113 | push_data = {switch=qs.switch} 114 | -- Switch Level 115 | elseif qs.level then 116 | led_lvl_ctl(tonumber(qs.level)) 117 | push_data = {level=qs.level} 118 | -- Color Control 119 | elseif qs.red or qs.green or qs.blue then 120 | led_clr_ctl( 121 | tonumber(qs.red), 122 | tonumber(qs.green), 123 | tonumber(qs.blue)) 124 | end 125 | 126 | -- If request came from 127 | -- browser control view 128 | if httpdata['user-agent'] ~= nil then 129 | -- Try to push state to 130 | -- Hub for bidirectional 131 | -- comms (device built-in 132 | -- /control page). 133 | if push_data ~= nil then 134 | push_state(push_data) 135 | end 136 | 137 | conn:send( 138 | res.ok_200(res.CONTROL_VIEW)) 139 | push_data = nil 140 | collectgarbage() 141 | return 142 | end 143 | -- Simple Response 144 | -- for socket comm 145 | -- on /control 146 | return conn:send( 147 | res.ok_200()) 148 | else 149 | -- wifi setting default 150 | conn:send( 151 | res.ok_200(res.WIFI_CONFIG_VIEW)) 152 | end -- end routing 153 | qs = nil 154 | httpdata = nil 155 | collectgarbage() 156 | end -- receive callback 157 | 158 | local server = net.createServer(net.TCP) 159 | server:listen(SRV_PORT, function(conn) 160 | conn:on('receive', recv_cb) 161 | conn:on('sent', function(conn) conn:close() end) -- close connection 162 | end) 163 | end 164 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/app/src/upnp.lua: -------------------------------------------------------------------------------- 1 | ------------------------- 2 | -- HTTP Response Parser 3 | local function httpparse(payload) 4 | local data = { status='', headers={} } 5 | data.status = payload:sub(0, payload:find('\r\n')) 6 | for k, v in payload:gmatch('(%w+[%w+-]+): ([%w+-: ()/=;]+)') do 7 | data.headers[k:lower()] = v 8 | end 9 | return data 10 | end 11 | ---------------- 12 | -- UPnP listener 13 | -- for SSDP flow 14 | function upnp_start() 15 | net.multicastJoin('', MC_ADDR) 16 | 17 | local SSDP_RES = table.concat({ 18 | 'HTTP/1.1 200 OK', 19 | 'Cache-Control: max-age=100', 20 | 'EXT:', 21 | 'SERVER: NodeMCU/Lua5.1.4 UPnP/1.1 '..DEV.NAME..'/0.1', 22 | 'ST: upnp:rootdevice', 23 | 'USN: uuid:'..DEV.CHIP_ID..'-'..DEV.SN, 24 | 'Location: http://'..wifi.sta.getip()..':80/'..DEV.NAME..'.xml' 25 | }, '\r\n') 26 | 27 | -- Listen on-demand M-SEARCH streams. 28 | local function recv_cb(conn, payload, port, ip) 29 | local req = httpparse(payload) 30 | local headers = req.headers 31 | 32 | print('INCOMING TRAFFIC:\r\n'..payload..'\r\n') 33 | if req and req.status:find('M-SEARCH') then 34 | if headers.st:find(DEV.MN) and headers.st:find(DEV.NAME) then 35 | --print('DISCOVERY STREAM:\r\n'..payload..'\r\n') 36 | print('SSDP RESPONSE:\r\n'..SSDP_RES..'\r\n') 37 | --Send SSDP repsonse 38 | conn:send(port, ip, SSDP_RES) 39 | SSDP_RES = nil 40 | req = nil 41 | collectgarbage() 42 | end 43 | end 44 | end 45 | 46 | -- Close UDP socket 47 | -- end session 48 | local function close_cb(conn) 49 | conn:close() 50 | net.multicastLeave('', MC_ADDR) 51 | end 52 | 53 | -- Init socket 54 | local upnp = net.createUDPSocket() 55 | upnp:on('receive', recv_cb) 56 | upnp:on('sent', close_cb) 57 | upnp:listen(MC_PORT, LOCAL_ADDR) 58 | end 59 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/config.yml: -------------------------------------------------------------------------------- 1 | name: 'LAN LightBulb' 2 | packageKey: 'LAN-LightBulb' 3 | permissions: 4 | lan: {} 5 | discovery: {} 6 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/profiles/LightBulb.yml: -------------------------------------------------------------------------------- 1 | name: LightBulb.v1 2 | components: 3 | - id: main 4 | capabilities: 5 | - id: switch 6 | version: 1 7 | - id: switchLevel 8 | version: 1 9 | - id: colorControl 10 | version: 1 11 | - id: refresh 12 | version: 1 13 | categories: 14 | - name: Light 15 | metadata: 16 | deviceType: Light 17 | ocfDeviceType: oic.d.light 18 | deviceTypeId: Light 19 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/commands.lua: -------------------------------------------------------------------------------- 1 | local caps = require('st.capabilities') 2 | local utils = require('st.utils') 3 | local neturl = require('net.url') 4 | local log = require('log') 5 | local json = require('dkjson') 6 | local cosock = require "cosock" 7 | local http = cosock.asyncify "socket.http" 8 | local ltn12 = require('ltn12') 9 | 10 | local command_handler = {} 11 | 12 | --------------- 13 | -- Ping command 14 | function command_handler.ping(address, port, device) 15 | local ping_data = {ip=address, port=port, ext_uuid=device.id} 16 | return command_handler.send_lan_command( 17 | device.device_network_id, 'POST', 'ping', ping_data) 18 | end 19 | ------------------ 20 | -- Refresh command 21 | function command_handler.refresh(_, device) 22 | local success, data = command_handler.send_lan_command( 23 | device.device_network_id, 24 | 'GET', 25 | 'refresh') 26 | 27 | -- Check success 28 | if success then 29 | -- Monkey patch due to issues 30 | -- on ltn12 lib to fully sink 31 | -- JSON payload into table. Last 32 | -- bracket is missing. 33 | -- 34 | -- Update below when fixed: 35 | --local raw_data = json.decode(table.concat(data)) 36 | local raw_data = json.decode(table.concat(data)..'}') 37 | local calc_lvl = math.floor((raw_data.lvl * 100)/255) 38 | 39 | -- Define online status 40 | device:online() 41 | 42 | -- Refresh Switch Level 43 | log.trace('Refreshing Switch Level') 44 | device:emit_event(caps.switchLevel.level(calc_lvl)) 45 | 46 | -- Refresh Switch 47 | log.trace('Refreshing Switch') 48 | if calc_lvl == 0 then 49 | device:emit_event(caps.switch.switch.off()) 50 | else 51 | device:emit_event(caps.switch.switch.on()) 52 | end 53 | 54 | -- Refresh Color Control 55 | log.trace('Refreshing Color Control') 56 | local calc_r = 255 - raw_data.clr.r 57 | local calc_g = 255 - raw_data.clr.g 58 | local calc_b = 255 - raw_data.clr.b 59 | local hue, sta = utils.rgb_to_hsl(calc_r, calc_g, calc_b) 60 | device:emit_event(caps.colorControl.saturation(sta)) 61 | device:emit_event(caps.colorControl.hue(hue)) 62 | else 63 | log.error('failed to poll device state') 64 | -- Set device as offline 65 | device:offline() 66 | end 67 | end 68 | 69 | ---------------- 70 | -- Switch commad 71 | function command_handler.on_off(_, device, command) 72 | local on_off = command.command 73 | -- send command via LAN 74 | local success = command_handler.send_lan_command( 75 | device.device_network_id, 76 | 'POST', 77 | 'control', 78 | {switch=on_off}) 79 | 80 | -- Check if success 81 | if success then 82 | if on_off == 'off' then 83 | return device:emit_event(caps.switch.switch.off()) 84 | end 85 | return device:emit_event(caps.switch.switch.on()) 86 | end 87 | log.error('no response from device') 88 | end 89 | 90 | ----------------------- 91 | -- Switch level command 92 | function command_handler.set_level(_, device, command) 93 | local lvl = command.args.level 94 | -- send command via LAN 95 | local success = command_handler.send_lan_command( 96 | device.device_network_id, 97 | 'POST', 98 | 'control', 99 | {level=lvl}) 100 | 101 | -- Check if success 102 | if success then 103 | if lvl == 0 then 104 | device:emit_event(caps.switch.switch.off()) 105 | else 106 | device:emit_event(caps.switch.switch.on()) 107 | end 108 | device:emit_event(caps.switchLevel.level(lvl)) 109 | return 110 | end 111 | log.error('no response from device') 112 | end 113 | 114 | ------------------------ 115 | -- Color control command 116 | function command_handler.set_color(_, device, command) 117 | local red, green, blue = utils.hsl_to_rgb( 118 | command.args.color.hue, command.args.color.saturation) 119 | 120 | local success = command_handler.send_lan_command( 121 | device.device_network_id, 122 | 'POST', 123 | 'control', 124 | {red=red, green=green, blue=blue}) 125 | 126 | -- Check if success 127 | if success then 128 | local hue, sta = utils.rgb_to_hsl(red, green, blue) 129 | device:emit_event(caps.switch.switch.on()) 130 | device:emit_event(caps.colorControl.saturation(sta)) 131 | device:emit_event(caps.colorControl.hue(hue)) 132 | return 133 | end 134 | log.error('no response from device') 135 | end 136 | 137 | ------------------------ 138 | -- Send LAN HTTP Request 139 | function command_handler.send_lan_command(url, method, path, body) 140 | local dest_url = url..'/'..path 141 | local query = neturl.buildQuery(body or {}) 142 | local res_body = {} 143 | 144 | -- HTTP Request 145 | local _, code = http.request({ 146 | method=method, 147 | url=dest_url..'?'..query, 148 | sink=ltn12.sink.table(res_body), 149 | headers={ 150 | ['Content-Type'] = 'application/x-www-urlencoded' 151 | }}) 152 | 153 | -- Handle response 154 | if code == 200 then 155 | return true, res_body 156 | end 157 | return false, nil 158 | end 159 | 160 | return command_handler 161 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/config.lua: -------------------------------------------------------------------------------- 1 | local config = {} 2 | -- device info 3 | -- NOTE: In the future this information 4 | -- may be submitted through the Developer 5 | -- Workspace to avoid hardcoded values. 6 | config.DEVICE_PROFILE='LightBulb.v1' 7 | config.DEVICE_TYPE='LAN' 8 | -- SSDP Config 9 | config.MC_ADDRESS='239.255.255.250' 10 | config.MC_PORT=1900 11 | config.MC_TIMEOUT=2 12 | config.MSEARCH=table.concat({ 13 | 'M-SEARCH * HTTP/1.1', 14 | 'HOST: 239.255.255.250:1900', 15 | 'MAN: "ssdp:discover"', 16 | 'MX: 4', 17 | 'ST: urn:SmartThingsCommunity:device:LightBulbESP8266:1' 18 | }, '\r\n') 19 | config.SCHEDULE_PERIOD=300 20 | return config 21 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/discovery.lua: -------------------------------------------------------------------------------- 1 | local socket = require('socket') 2 | local cosock = require "cosock" 3 | local http = cosock.asyncify "socket.http" 4 | local ltn12 = require('ltn12') 5 | local log = require('log') 6 | local config = require('config') 7 | -- XML modules 8 | local xml2lua = require "xml2lua" 9 | local xml_handler = require "xmlhandler.tree" 10 | 11 | ----------------------- 12 | -- SSDP Response parser 13 | local function parse_ssdp(data) 14 | local res = {} 15 | res.status = data:sub(0, data:find('\r\n')) 16 | for k, v in data:gmatch('([%w-]+): ([%a+-: /=]+)') do 17 | res[k:lower()] = v 18 | end 19 | return res 20 | end 21 | 22 | -- Fetching device metadata via 23 | -- /.xml 24 | -- from SSDP Response Location header 25 | local function fetch_device_info(url) 26 | log.info('===== FETCHING DEVICE METADATA...') 27 | local res = {} 28 | local _, status = http.request({ 29 | url=url, 30 | sink=ltn12.sink.table(res) 31 | }) 32 | 33 | -- XML Parser 34 | local xmlres = xml_handler:new() 35 | local xml_parser = xml2lua.parser(xmlres) 36 | xml_parser:parse(table.concat(res)) 37 | 38 | -- Device metadata 39 | local meta = xmlres.root.root.device 40 | 41 | if not xmlres.root or not meta then 42 | log.error('===== FAILED TO FETCH METADATA AT: '..url) 43 | return nil 44 | end 45 | 46 | return { 47 | name=meta.friendlyName, 48 | vendor=meta.UDN, 49 | mn=meta.manufacturer, 50 | model=meta.modelName, 51 | location=url:sub(0, url:find('/'..meta.friendlyName)-1) 52 | } 53 | end 54 | 55 | -- This function enables a UDP 56 | -- Socket and broadcast a single 57 | -- M-SEARCH request, i.e., it 58 | -- must be looped appart. 59 | local function find_device() 60 | -- UDP socket initialization 61 | local upnp = socket.udp() 62 | upnp:setsockname('*', 0) 63 | upnp:setoption('broadcast', true) 64 | upnp:settimeout(config.MC_TIMEOUT) 65 | 66 | -- broadcasting request 67 | log.info('===== SCANNING NETWORK...') 68 | upnp:sendto(config.MSEARCH, config.MC_ADDRESS, config.MC_PORT) 69 | 70 | -- Socket will wait n seconds 71 | -- based on the s:setoption(n) 72 | -- to receive a response back. 73 | local res = upnp:receivefrom() 74 | 75 | -- close udp socket 76 | upnp:close() 77 | 78 | if res ~= nil then 79 | return res 80 | end 81 | return nil 82 | end 83 | 84 | local function create_device(driver, device) 85 | log.info('===== CREATING DEVICE...') 86 | log.info('===== DEVICE DESTINATION ADDRESS: '..device.location) 87 | -- device metadata table 88 | local metadata = { 89 | type = config.DEVICE_TYPE, 90 | device_network_id = device.location, 91 | label = device.name, 92 | profile = config.DEVICE_PROFILE, 93 | manufacturer = device.mn, 94 | model = device.model, 95 | vendor_provided_label = device.UDN 96 | } 97 | return driver:try_create_device(metadata) 98 | end 99 | 100 | -- Discovery service which will 101 | -- invoke the above private functions. 102 | -- - find_device 103 | -- - parse_ssdp 104 | -- - fetch_device_info 105 | -- - create_device 106 | -- 107 | -- This resource is linked to 108 | -- driver.discovery and it is 109 | -- automatically called when 110 | -- user scan devices from the 111 | -- SmartThings App. 112 | local disco = {} 113 | function disco.start(driver, opts, cons) 114 | while true do 115 | local device_res = find_device() 116 | 117 | if device_res ~= nil then 118 | device_res = parse_ssdp(device_res) 119 | log.info('===== DEVICE FOUND IN NETWORK...') 120 | log.info('===== DEVICE DESCRIPTION AT: '..device_res.location) 121 | 122 | local device = fetch_device_info(device_res.location) 123 | return create_device(driver, device) 124 | end 125 | log.error('===== DEVICE NOT FOUND IN NETWORK') 126 | end 127 | end 128 | 129 | return disco 130 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/init.lua: -------------------------------------------------------------------------------- 1 | local Driver = require('st.driver') 2 | local caps = require('st.capabilities') 3 | 4 | -- local imports 5 | local discovery = require('discovery') 6 | local lifecycles = require('lifecycles') 7 | local commands = require('commands') 8 | local server = require('server') 9 | 10 | -------------------- 11 | -- Driver definition 12 | local driver = 13 | Driver( 14 | 'LAN-LightBulb', 15 | { 16 | discovery = discovery.start, 17 | lifecycle_handlers = lifecycles, 18 | supported_capabilities = { 19 | caps.switch, 20 | caps.switchLevel, 21 | caps.colorControl, 22 | caps.refresh 23 | }, 24 | capability_handlers = { 25 | -- Switch command handler 26 | [caps.switch.ID] = { 27 | [caps.switch.commands.on.NAME] = commands.on_off, 28 | [caps.switch.commands.off.NAME] = commands.on_off 29 | }, 30 | -- Switch Level command handler 31 | [caps.switchLevel.ID] = { 32 | [caps.switchLevel.commands.setLevel.NAME] = commands.set_level 33 | }, 34 | -- Color Control command handler 35 | [caps.colorControl.ID] = { 36 | [caps.colorControl.commands.setColor.NAME] = commands.set_color 37 | }, 38 | -- Refresh command handler 39 | [caps.refresh.ID] = { 40 | [caps.refresh.commands.refresh.NAME] = commands.refresh 41 | } 42 | } 43 | } 44 | ) 45 | 46 | --------------------------------------- 47 | -- Switch control for external commands 48 | function driver:on_off(device, on_off) 49 | if on_off == 'off' then 50 | return device:emit_event(caps.switch.switch.off()) 51 | end 52 | return device:emit_event(caps.switch.switch.on()) 53 | end 54 | 55 | --------------------------------------------- 56 | -- Switch level control for external commands 57 | function driver:set_level(device, lvl) 58 | if lvl == 0 then 59 | device:emit_event(caps.switch.switch.off()) 60 | else 61 | device:emit_event(caps.switch.switch.on()) 62 | end 63 | return device:emit_event(caps.switchLevel.level(lvl)) 64 | end 65 | 66 | ----------------------------- 67 | -- Initialize Hub server 68 | -- that will open port to 69 | -- allow bidirectional comms. 70 | server.start(driver) 71 | 72 | -------------------- 73 | -- Initialize Driver 74 | driver:run() 75 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/lifecycles.lua: -------------------------------------------------------------------------------- 1 | local commands = require('commands') 2 | local config = require('config') 3 | 4 | local lifecycle_handler = {} 5 | 6 | function lifecycle_handler.init(driver, device) 7 | ------------------- 8 | -- Set up scheduled 9 | -- services once the 10 | -- driver gets 11 | -- initialized. 12 | 13 | -- Ping schedule. 14 | device.thread:call_on_schedule( 15 | config.SCHEDULE_PERIOD, 16 | function () 17 | return commands.ping( 18 | driver.server.ip, 19 | driver.server.port, 20 | device) 21 | end, 22 | 'Ping schedule') 23 | 24 | -- Refresh schedule 25 | device.thread:call_on_schedule( 26 | config.SCHEDULE_PERIOD, 27 | function () 28 | return commands.refresh(nil, device) 29 | end, 30 | 'Refresh schedule') 31 | end 32 | 33 | function lifecycle_handler.added(driver, device) 34 | -- Once device has been created 35 | -- at API level, poll its state 36 | -- via refresh command and send 37 | -- request to share server's ip 38 | -- and port to the device os it 39 | -- can communicate back. 40 | commands.refresh(nil, device) 41 | commands.ping(driver.server.ip, driver.server.port, device) 42 | end 43 | 44 | function lifecycle_handler.removed(_, device) 45 | -- Notify device that the device 46 | -- instance has been deleted and 47 | -- parent node must be deleted at 48 | -- device app. 49 | commands.send_lan_command( 50 | device.device_network_id, 51 | 'POST', 52 | 'delete') 53 | 54 | -- Remove Schedules created under 55 | -- device.thread to avoid unnecessary 56 | -- CPU processing. 57 | for timer in pairs(device.thread.timers) do 58 | device.thread:cancel_timer(timer) 59 | end 60 | end 61 | 62 | return lifecycle_handler 63 | -------------------------------------------------------------------------------- /lightbulb-lan-esp8266/driver/src/server.lua: -------------------------------------------------------------------------------- 1 | local lux = require('luxure') 2 | local cosock = require('cosock').socket 3 | local json = require('dkjson') 4 | 5 | local hub_server = {} 6 | 7 | function hub_server.start(driver) 8 | local server = lux.Server.new_with(cosock.tcp(), {env='debug'}) 9 | 10 | -- Register server 11 | driver:register_channel_handler(server.sock, function () 12 | server:tick() 13 | end) 14 | 15 | -- Endpoint 16 | server:post('/push-state', function (req, res) 17 | local body = json.decode(req:get_body()) 18 | 19 | local device = driver:get_device_info(body.uuid) 20 | if body.switch then 21 | driver:on_off(device, body.switch) 22 | elseif body.level then 23 | driver:set_level(device, tonumber(body.level)) 24 | end 25 | res:send('HTTP/1.1 200 OK') 26 | end) 27 | server:listen() 28 | driver.server = server 29 | end 30 | 31 | return hub_server 32 | -------------------------------------------------------------------------------- /st-multipurpose-sensor/README.md: -------------------------------------------------------------------------------- 1 | # Sample Edge Driver for SmartThings Multipurpose Sensor 2 | 3 | Model: IM6001-MPP04 4 | 5 | Protocol: Zigbee 6 | 7 | ## Prerequisites 8 | 9 | 1. Set up the SmartThings CLI according to the [configuration document](https://github.com/SmartThingsCommunity/smartthings-cli/blob/master/packages/cli/doc/configuration.md). 10 | 2. Add the [Edge Driver plugin](https://github.com/SmartThingsCommunity/edge-alpha-cli-plugin#set-up) to the CLI. 11 | 3. Configure your development environment for the [SmartThingsEdgeDrivers](https://github.com/SmartThingsCommunity/SmartThingsEdgeDriversBeta) 12 | 4. A SmartThings hub with firmware version 000.038.000XX or greater and a SmartThings Multipurpose sensor (Zigbee). 13 | 14 | ## Uploading Your Driver to SmartThings 15 | 16 | _Note: Review the installation tutorial in our [Developer's Community](https://community.smartthings.com/t/creating-drivers-for-zigbee-devices-with-smartthings-edge/229502)._ 17 | 18 | 1. Compile the driver: 19 | 20 | ``` 21 | smartthings edge:drivers:package driver/ 22 | ``` 23 | 24 | 2. Next, create a channel for your driver 25 | 26 | ``` 27 | smartthings edge:channels:create 28 | ``` 29 | 30 | 3. Enroll your driver into the channel 31 | 32 | ``` 33 | smartthings edge:channels:enroll 34 | ``` 35 | 36 | 4. Publish your driver to the channel 37 | 38 | ``` 39 | smartthings edge:drivers:publish 40 | ``` 41 | 42 | 5. If the package was successfully created, you can call the command below and follow the on-screen prompts to install the Driver in your Hub: 43 | 44 | ``` 45 | smartthings edge:drivers:install 46 | ``` 47 | 48 | You should see the confirmation message: "Driver {driver-id} installed to Hub {hub-id}" 49 | 50 | 6. Use your WiFi router or the [SmartThings IDE](https://account.smartthings.com/login) > My Hubs to locate and copy the IP Address for your Hub. 51 | 52 | 7. From a computer on the same local network as your Hub, open a new terminal window and run the command to get the logs from all the installed drivers. 53 | 54 | ``` 55 | smartthings edge:drivers:logcat --hub-address=x.x.x.x -a 56 | ``` 57 | 58 | ## Onboarding your New Device 59 | 60 | 1. Open the SmartThings App and go to the location where the hub is installed. 61 | 2. Go to Add (+) > Device or select _Scan Nearby_ (If you have more than one, select the corresponding Hub as well) 62 | 63 | 3. Put your device in pairing mode; the specifications will vary by manufacturer (for the SmartThings Multipurpose sensor, press the device’s reset button once). 64 | 4. Keep the terminal view open until you see only reporting values messages in the logs. 65 | 66 | Example Output 67 | 68 | ```text 69 | emitting event: {"attribute_id":"temperature","component_id":"main","state":{"unit":"C","value":28.66},"capability_id":"temperatureMeasurement"} 70 | ``` 71 | 72 | If your Device paired correctly and the Driver was applied, you should not see any errors in the logs (including "UNSUPPORTED" responses to any Zigbee TX message). You can validate this by opening the SmartThings app and controlling and/or viewing all of the devices Capabilities (e.g., open/close or change the temperature). 73 | -------------------------------------------------------------------------------- /st-multipurpose-sensor/config.yml: -------------------------------------------------------------------------------- 1 | name: 'SmartThings Multipurpose Sensor' 2 | packageKey: 'smartthingsMultipurposeSensor' 3 | permissions: 4 | zigbee: {} 5 | -------------------------------------------------------------------------------- /st-multipurpose-sensor/fingerprints.yml: -------------------------------------------------------------------------------- 1 | zigbeeManufacturer: 2 | - id: "Samjin/multi" 3 | deviceLabel: Multipurpose Sensor 4 | manufacturer: Samjin 5 | model: multi 6 | deviceProfileName: st-multipurpose 7 | - id: "SmartThings/multiv4" 8 | deviceLabel: Multipurpose Sensor 9 | manufacturer: SmartThings 10 | model: multiv4 11 | deviceProfileName: st-multipurpose -------------------------------------------------------------------------------- /st-multipurpose-sensor/profiles/multipurpose-profile.yml: -------------------------------------------------------------------------------- 1 | name: st-multipurpose 2 | components: 3 | - id: main 4 | capabilities: 5 | - id: contactSensor 6 | version: 1 7 | - id: temperatureMeasurement 8 | version: 1 9 | - id: battery 10 | version: 1 11 | - id: threeAxis 12 | version: 1 13 | - id: accelerationSensor 14 | version: 1 15 | - id: refresh 16 | version: 1 17 | - id: healthCheck 18 | version: 1 19 | categories: 20 | - name: MultiFunctionalSensor 21 | preferences: 22 | - preferenceId: tempOffset 23 | explicit: true 24 | - name: "garageSensor" 25 | title: "Use on garage door" 26 | required: false 27 | preferenceType: enumeration 28 | definition: 29 | options: 30 | "Yes" : "Yes" 31 | "No" : "No" 32 | default: "No" -------------------------------------------------------------------------------- /st-multipurpose-sensor/src/common.lua: -------------------------------------------------------------------------------- 1 | local capabilities = require "st.capabilities" 2 | local data_types = require "st.zigbee.data_types" 3 | local threeAxis = capabilities.threeAxis 4 | local utils = require "st.utils" 5 | local log = require "log" 6 | 7 | local common = {} 8 | 9 | local INTERIM_XYZ = "interim_xyz" 10 | 11 | common.MFG_CLUSTER = 0xFC02 12 | common.MFG_CODES = { 13 | Samjin = 0x1241, 14 | SmartThings = 0x110A, 15 | Centralite = 0x104E 16 | } 17 | common.ACCELERATION_ATTR_ID = 0x0010 18 | common.X_AXIS_ATTR_ID = 0x0012 19 | common.Y_AXIS_ATTR_ID = 0x0013 20 | common.Z_AXIS_ATTR_ID = 0x0014 21 | 22 | function common.axis_handler(axis_index, invert) 23 | return function(driver, device, value, zb_rx) 24 | local current_value = device:get_latest_state("main", threeAxis.ID, threeAxis.threeAxis.NAME, device:get_field(INTERIM_XYZ)) 25 | if current_value == nil then current_value = {} end 26 | current_value[axis_index] = not invert and value.value or value.value * -1 27 | log.trace("current_value ",utils.stringify_table(current_value)) 28 | if utils.table_size(current_value) == 3 then -- emit an event when we have a value for all three directions (installation) 29 | -- elements have to be broken out this way because of the translation to storage and back 30 | device:emit_event(threeAxis.threeAxis({current_value[1], current_value[2], current_value[3]})) 31 | device:set_field(INTERIM_XYZ,nil) 32 | else 33 | device:set_field(INTERIM_XYZ, current_value) 34 | end 35 | 36 | if axis_index == 3 and device.preferences.garageSensor == "Yes" then 37 | -- if this is the z-index and we're using as a garage door, send contact events 38 | if math.abs(value.value) > 900 then 39 | device:emit_event(capabilities.contactSensor.contact.closed()) 40 | elseif math.abs(value.value) < 100 then 41 | device:emit_event(capabilities.contactSensor.contact.open()) 42 | end 43 | end 44 | end 45 | end 46 | 47 | function common.acceleration_handler(driver, device, value, zb_rx) 48 | device:emit_event(capabilities.accelerationSensor.acceleration(value.value == 1 and "active" or "inactive")) 49 | end 50 | 51 | function common.get_cluster_configurations(manufacturer) 52 | return { 53 | [capabilities.accelerationSensor.ID] = { 54 | { 55 | cluster = common.MFG_CLUSTER, 56 | attribute = common.ACCELERATION_ATTR_ID, 57 | minimum_interval = 0, 58 | maximum_interval = 300, 59 | reportable_change = 1, 60 | data_type = data_types.Bitmap8, 61 | mfg_code = common.MFG_CODES[manufacturer] 62 | } 63 | }, 64 | [capabilities.threeAxis.ID] = { 65 | { 66 | cluster = common.MFG_CLUSTER, 67 | attribute = common.X_AXIS_ATTR_ID, 68 | minimum_interval = 0, 69 | maximum_interval = 300, 70 | reportable_change = 0x0001, 71 | data_type = data_types.Int16, 72 | mfg_code = common.MFG_CODES[manufacturer] 73 | }, 74 | { 75 | cluster = common.MFG_CLUSTER, 76 | attribute = common.Y_AXIS_ATTR_ID, 77 | minimum_interval = 0, 78 | maximum_interval = 300, 79 | reportable_change = 0x0001, 80 | data_type = data_types.Int16, 81 | mfg_code = common.MFG_CODES[manufacturer] 82 | }, 83 | { 84 | cluster = common.MFG_CLUSTER, 85 | attribute = common.Z_AXIS_ATTR_ID, 86 | minimum_interval = 0, 87 | maximum_interval = 300, 88 | reportable_change = 0x0001, 89 | data_type = data_types.Int16, 90 | mfg_code = common.MFG_CODES[manufacturer] 91 | } 92 | } 93 | } 94 | end 95 | 96 | return common 97 | -------------------------------------------------------------------------------- /st-multipurpose-sensor/src/init.lua: -------------------------------------------------------------------------------- 1 | local capabilities = require "st.capabilities" 2 | local zcl_clusters = require "st.zigbee.zcl.clusters" 3 | local ZigbeeDriver = require "st.zigbee" 4 | local constants = require "st.zigbee.constants" 5 | local defaults = require "st.zigbee.defaults" 6 | local contact_sensor_defaults = require "st.zigbee.defaults.contactSensor_defaults" 7 | local data_types = require "st.zigbee.data_types" 8 | local common = require("common") 9 | 10 | local function ias_zone_status_change_handler(driver, device, zb_rx) 11 | if (device.preferences.garageSensor ~= "Yes") then 12 | contact_sensor_defaults.ias_zone_status_change_handler(driver, device, zb_rx) 13 | end 14 | end 15 | 16 | local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) 17 | if (device.preferences.garageSensor ~= "Yes") then 18 | contact_sensor_defaults.ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) 19 | end 20 | end 21 | 22 | local function added(driver, device) 23 | --Add the manufacturer-specific attributes to generate their configure reporting and bind requests 24 | for capability_id, configs in pairs(common.get_cluster_configurations(device:get_manufacturer())) do 25 | if device:supports_capability_by_id(capability_id) then 26 | for _, config in pairs(configs) do 27 | device:add_configured_attribute(config) 28 | device:add_monitored_attribute(config) 29 | end 30 | end 31 | end 32 | end 33 | 34 | ----------------------Driver configuration---------------------- 35 | local handlers = { 36 | global = {}, 37 | cluster = { 38 | [zcl_clusters.IASZone.ID] = { 39 | [zcl_clusters.IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler 40 | } 41 | }, 42 | attr = { 43 | [common.MFG_CLUSTER] = { 44 | [common.ACCELERATION_ATTR_ID] = common.acceleration_handler, 45 | [common.X_AXIS_ATTR_ID] = common.axis_handler(2, false), 46 | [common.Y_AXIS_ATTR_ID] = common.axis_handler(3, false), 47 | [common.Z_AXIS_ATTR_ID] = common.axis_handler(1, false) 48 | }, 49 | [zcl_clusters.IASZone.ID] = { 50 | [zcl_clusters.IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler 51 | } 52 | }, 53 | zdo = {} 54 | } 55 | 56 | local zigbee_multipurpose_driver_template = { 57 | supported_capabilities = { 58 | capabilities.contactSensor, 59 | capabilities.battery, 60 | capabilities.temperatureMeasurement, 61 | capabilities.threeAxis, 62 | capabilities.accelerationSensor, 63 | capabilities.refresh 64 | }, 65 | zigbee_handlers = handlers, 66 | lifecycle_handlers = { 67 | added = added 68 | }, 69 | ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, 70 | sub_drivers = { require("smartthings") } 71 | } 72 | 73 | --Run driver 74 | defaults.register_for_default_handlers(zigbee_multipurpose_driver_template, zigbee_multipurpose_driver_template.supported_capabilities) 75 | local zigbee_multipurpose = ZigbeeDriver("smartthingsMultipurposeSensor", zigbee_multipurpose_driver_template) 76 | zigbee_multipurpose:run() -------------------------------------------------------------------------------- /st-multipurpose-sensor/src/smartthings/init.lua: -------------------------------------------------------------------------------- 1 | local zcl_clusters = require "st.zigbee.zcl.clusters" 2 | local capabilities = require "st.capabilities" 3 | local battery = capabilities.battery 4 | local battery_defaults = require "st.zigbee.defaults.battery_defaults" 5 | local common = require("common") 6 | local utils = require "st.utils" 7 | 8 | local can_handle = function(opts, driver, device) 9 | return device:get_manufacturer() == "SmartThings" 10 | end 11 | 12 | local battery_handler = function(driver, device, value, zb_rx) 13 | local batteryMap = {[28] = 100, [27] = 100, [26] = 100, [25] = 90, [24] = 90, [23] = 70, 14 | [22] = 70, [21] = 50, [20] = 50, [19] = 30, [18] = 30, [17] = 15, [16] = 1, [15] = 0} 15 | local minVolts = 15 16 | local maxVolts = 28 17 | 18 | value = utils.clamp_value(value.value, minVolts, maxVolts) 19 | 20 | device:emit_event(battery.battery(batteryMap[value])) 21 | end 22 | 23 | local smartthings_multi_sensor = { 24 | NAME = "SmartThings multi sensor", 25 | zigbee_handlers = { 26 | attr = { 27 | [common.MFG_CLUSTER] = { 28 | [common.ACCELERATION_ATTR_ID] = common.acceleration_handler, 29 | [common.X_AXIS_ATTR_ID] = common.axis_handler(3, true), -- lua indexes from 1 30 | [common.Y_AXIS_ATTR_ID] = common.axis_handler(2, false), 31 | [common.Z_AXIS_ATTR_ID] = common.axis_handler(1, false) 32 | }, 33 | [zcl_clusters.PowerConfiguration.ID] = { 34 | [zcl_clusters.PowerConfiguration.attributes.BatteryVoltage.ID] = battery_handler 35 | } 36 | } 37 | }, 38 | lifecycle_handlers = { 39 | init = battery_defaults.build_linear_voltage_init(2.3, 3.0) 40 | }, 41 | can_handle = can_handle 42 | } 43 | 44 | return smartthings_multi_sensor 45 | -------------------------------------------------------------------------------- /st-multipurpose-sensor/src/test/test_smartthings_sensor.lua: -------------------------------------------------------------------------------- 1 | -- Mock out globals 2 | local test = require "integration_test" 3 | local clusters = require "st.zigbee.zcl.clusters" 4 | local IASZone = clusters.IASZone 5 | local TemperatureMeasurement = clusters.TemperatureMeasurement 6 | local PowerConfiguration = clusters.PowerConfiguration 7 | local cluster_base = require "st.zigbee.cluster_base" 8 | local data_types = require "st.zigbee.data_types" 9 | local capabilities = require "st.capabilities" 10 | local zigbee_test_utils = require "integration_test.zigbee_test_utils" 11 | local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" 12 | local base64 = require "st.base64" 13 | local t_utils = require "integration_test.utils" 14 | 15 | local ZoneStatusAttribute = IASZone.attributes.ZoneStatus 16 | 17 | local MFG_CLUSTER = 0xFC02 --for Acceleration and Three axis 18 | local MFG_CODE = 0x110A 19 | local ACCELERATION_ATTR_ID = 0x0010 20 | local X_AXIS_ATTR_ID = 0x0012 21 | local Y_AXIS_ATTR_ID = 0x0013 22 | local Z_AXIS_ATTR_ID = 0x0014 23 | 24 | local mock_device = test.mock_device.build_test_zigbee_device( 25 | { 26 | profile = t_utils.get_profile_definition("multipurpose-profile.yml"), 27 | zigbee_endpoints = { 28 | [1] = { 29 | id = 1, 30 | manufacturer = "SmartThings", 31 | model = "multiv4", 32 | server_clusters = {0x0000,0x0001,0x0003,0x000F,0x0020,0x0402,0x0500,0xFC02} 33 | } 34 | } 35 | } 36 | ) 37 | zigbee_test_utils.prepare_zigbee_env_info() 38 | local function test_init() 39 | test.mock_device.add_test_device(mock_device) 40 | zigbee_test_utils.init_noop_health_check_timer() 41 | end 42 | test.set_test_init_function(test_init) 43 | 44 | test.register_message_test( 45 | "ThreeAxis report should be handled", 46 | { 47 | { 48 | channel = "zigbee", 49 | direction = "receive", 50 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{X_AXIS_ATTR_ID, data_types.Int16.ID, -90}}, MFG_CODE) } 51 | }, 52 | { 53 | channel = "zigbee", 54 | direction = "receive", 55 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{Y_AXIS_ATTR_ID, data_types.Int16.ID, 90}}, MFG_CODE) } 56 | }, 57 | { 58 | channel = "zigbee", 59 | direction = "receive", 60 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{Z_AXIS_ATTR_ID, data_types.Int16.ID, 45}}, MFG_CODE) } 61 | }, 62 | { 63 | channel = "capability", 64 | direction = "send", 65 | message = mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({45,90,90})) 66 | } 67 | } 68 | ) 69 | 70 | test.register_message_test( 71 | "Acceleration report should be handled", 72 | { 73 | { 74 | channel = "zigbee", 75 | direction = "receive", 76 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{ACCELERATION_ATTR_ID, data_types.Bitmap8.ID, 0x1}}, MFG_CODE) } 77 | }, 78 | { 79 | channel = "capability", 80 | direction = "send", 81 | message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active()) 82 | } 83 | } 84 | ) 85 | 86 | test.register_coroutine_test( 87 | "Configure should configure all necessary attributes", 88 | function () 89 | test.socket.zigbee:__set_channel_ordering("relaxed") 90 | test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) 91 | test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) 92 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) 93 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) 94 | test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) 95 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {ACCELERATION_ATTR_ID}, MFG_CODE) }) 96 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {X_AXIS_ATTR_ID}, MFG_CODE) }) 97 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Y_AXIS_ATTR_ID}, MFG_CODE) }) 98 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Z_AXIS_ATTR_ID}, MFG_CODE) }) 99 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) 100 | test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 30, 300, 0x10) }) 101 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, TemperatureMeasurement.ID) }) 102 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, ACCELERATION_ATTR_ID, 0, 300, data_types.Bitmap8, 1, MFG_CODE) }) 103 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, X_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 104 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, Y_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 105 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, Z_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 106 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, MFG_CLUSTER) }) 107 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1) }) 108 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1) }) 109 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) 110 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) }) 111 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) }) 112 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 300, 0) }) 113 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, IASZone.ID) }) 114 | 115 | mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 116 | end 117 | ) 118 | 119 | test.register_message_test( 120 | "Refresh should read all necessary attributes", 121 | { 122 | { 123 | channel = "device_lifecycle", 124 | direction = "receive", 125 | message = {mock_device.id, "added"} 126 | }, 127 | { 128 | channel = "capability", 129 | direction = "receive", 130 | message = { 131 | mock_device.id, 132 | { capability = "refresh", component = "main", command = "refresh", args = {} } 133 | } 134 | }, 135 | { 136 | channel = "zigbee", 137 | direction = "send", 138 | message = { 139 | mock_device.id, 140 | PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 141 | } 142 | }, 143 | { 144 | channel = "zigbee", 145 | direction = "send", 146 | message = { 147 | mock_device.id, 148 | PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) 149 | } 150 | }, 151 | { 152 | channel = "zigbee", 153 | direction = "send", 154 | message = { 155 | mock_device.id, 156 | TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) 157 | } 158 | }, 159 | { 160 | channel = "zigbee", 161 | direction = "send", 162 | message = { 163 | mock_device.id, 164 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Z_AXIS_ATTR_ID}, MFG_CODE) 165 | } 166 | }, 167 | { 168 | channel = "zigbee", 169 | direction = "send", 170 | message = { 171 | mock_device.id, 172 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {X_AXIS_ATTR_ID}, MFG_CODE) 173 | } 174 | }, 175 | { 176 | channel = "zigbee", 177 | direction = "send", 178 | message = { 179 | mock_device.id, 180 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Y_AXIS_ATTR_ID}, MFG_CODE) 181 | } 182 | }, 183 | { 184 | channel = "zigbee", 185 | direction = "send", 186 | message = { 187 | mock_device.id, 188 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {ACCELERATION_ATTR_ID}, MFG_CODE) 189 | } 190 | }, 191 | { 192 | channel = "zigbee", 193 | direction = "send", 194 | message = { 195 | mock_device.id, 196 | IASZone.attributes.ZoneStatus:read(mock_device) 197 | } 198 | }, 199 | }, 200 | { 201 | inner_block_ordering = "relaxed" 202 | } 203 | ) 204 | 205 | test.register_message_test( 206 | "Battery voltage report should be handled", 207 | { 208 | { 209 | channel = "zigbee", 210 | direction = "receive", 211 | message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 212 | 25) } 213 | }, 214 | { 215 | channel = "capability", 216 | direction = "send", 217 | message = mock_device:generate_test_message("main", capabilities.battery.battery(90)) 218 | } 219 | } 220 | ) 221 | 222 | test.run_registered_tests() -------------------------------------------------------------------------------- /st-multipurpose-sensor/src/test/test_threeAxis_sensor.lua: -------------------------------------------------------------------------------- 1 | -- Mock out globals 2 | local test = require "integration_test" 3 | local clusters = require "st.zigbee.zcl.clusters" 4 | local IASZone = clusters.IASZone 5 | local TemperatureMeasurement = clusters.TemperatureMeasurement 6 | local PowerConfiguration = clusters.PowerConfiguration 7 | local cluster_base = require "st.zigbee.cluster_base" 8 | local data_types = require "st.zigbee.data_types" 9 | local capabilities = require "st.capabilities" 10 | local zigbee_test_utils = require "integration_test.zigbee_test_utils" 11 | local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" 12 | local base64 = require "st.base64" 13 | local t_utils = require "integration_test.utils" 14 | 15 | local ZoneStatusAttribute = IASZone.attributes.ZoneStatus 16 | 17 | local MFG_CLUSTER = 0xFC02 --for Acceleration and Three axis 18 | local MFG_CODE = 0x1241 19 | local ACCELERATION_ATTR_ID = 0x0010 20 | local X_AXIS_ATTR_ID = 0x0012 21 | local Y_AXIS_ATTR_ID = 0x0013 22 | local Z_AXIS_ATTR_ID = 0x0014 23 | 24 | local mock_device = test.mock_device.build_test_zigbee_device( 25 | { 26 | profile = t_utils.get_profile_definition("multipurpose-profile.yml"), 27 | zigbee_endpoints = { 28 | [1] = { 29 | id = 1, 30 | manufacturer = "Samjin", 31 | model = "multi", 32 | server_clusters = {0x0000,0x0001,0x0003,0x0020,0x0402,0x0500,0xFC02} 33 | } 34 | } 35 | } 36 | ) 37 | zigbee_test_utils.prepare_zigbee_env_info() 38 | local function test_init() 39 | test.mock_device.add_test_device(mock_device) 40 | zigbee_test_utils.init_noop_health_check_timer() 41 | end 42 | test.set_test_init_function(test_init) 43 | 44 | test.register_message_test( 45 | "Temperature report should be handled (C)", 46 | { 47 | { 48 | channel = "zigbee", 49 | direction = "receive", 50 | message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } 51 | }, 52 | { 53 | channel = "capability", 54 | direction = "send", 55 | message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) 56 | } 57 | } 58 | ) 59 | 60 | test.register_message_test( 61 | "Battery percentage report should be handled", 62 | { 63 | { 64 | channel = "zigbee", 65 | direction = "receive", 66 | message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 55) } 67 | }, 68 | { 69 | channel = "capability", 70 | direction = "send", 71 | message = mock_device:generate_test_message("main", capabilities.battery.battery(28)) 72 | } 73 | } 74 | ) 75 | 76 | test.register_message_test( 77 | "Acceleration report should be handled", 78 | { 79 | { 80 | channel = "zigbee", 81 | direction = "receive", 82 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{ACCELERATION_ATTR_ID, data_types.Bitmap8.ID, 0x1}}, MFG_CODE) } 83 | }, 84 | { 85 | channel = "capability", 86 | direction = "send", 87 | message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active()) 88 | } 89 | } 90 | ) 91 | 92 | test.register_message_test( 93 | "ThreeAxis report should be handled", 94 | { 95 | { 96 | channel = "zigbee", 97 | direction = "receive", 98 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{X_AXIS_ATTR_ID, data_types.Int16.ID, -90}}, MFG_CODE) } 99 | }, 100 | { 101 | channel = "zigbee", 102 | direction = "receive", 103 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{Y_AXIS_ATTR_ID, data_types.Int16.ID, 90}}, MFG_CODE) } 104 | }, 105 | { 106 | channel = "zigbee", 107 | direction = "receive", 108 | message = { mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{Z_AXIS_ATTR_ID, data_types.Int16.ID, 45}}, MFG_CODE) } 109 | }, 110 | { 111 | channel = "capability", 112 | direction = "send", 113 | message = mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({45,-90,90})) 114 | } 115 | } 116 | ) 117 | 118 | test.register_coroutine_test( 119 | "Health check should check all relevant attributes", 120 | function() 121 | test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) 122 | test.wait_for_events() 123 | 124 | test.mock_time.advance_time(50000) -- 21600 is the battery max interval 125 | test.socket.zigbee:__set_channel_ordering("relaxed") 126 | test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) 127 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) 128 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) 129 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {ACCELERATION_ATTR_ID}, MFG_CODE) }) 130 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {X_AXIS_ATTR_ID}, MFG_CODE) }) 131 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Y_AXIS_ATTR_ID}, MFG_CODE) }) 132 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Z_AXIS_ATTR_ID}, MFG_CODE) }) 133 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) 134 | end, 135 | { 136 | test_init = function() 137 | test.mock_device.add_test_device(mock_device) 138 | test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") 139 | end 140 | } 141 | ) 142 | 143 | test.register_coroutine_test( 144 | "Configure should configure all necessary attributes", 145 | function () 146 | test.socket.zigbee:__set_channel_ordering("relaxed") 147 | test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) 148 | test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) 149 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) 150 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) 151 | test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) 152 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {ACCELERATION_ATTR_ID}, MFG_CODE) }) 153 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {X_AXIS_ATTR_ID}, MFG_CODE) }) 154 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Y_AXIS_ATTR_ID}, MFG_CODE) }) 155 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Z_AXIS_ATTR_ID}, MFG_CODE) }) 156 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) 157 | test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 30, 300, 0x10) }) 158 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, TemperatureMeasurement.ID) }) 159 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, ACCELERATION_ATTR_ID, 0, 300, data_types.Bitmap8, 1, MFG_CODE) }) 160 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, X_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 161 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, Y_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 162 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_attr_config(mock_device, MFG_CLUSTER, Z_AXIS_ATTR_ID, 0, 300, data_types.Int16, 1, MFG_CODE) }) 163 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, MFG_CLUSTER) }) 164 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1) }) 165 | test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1) }) 166 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) 167 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) }) 168 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) }) 169 | test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 300, 0) }) 170 | test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, IASZone.ID) }) 171 | 172 | mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 173 | end 174 | ) 175 | 176 | test.register_message_test( 177 | "Refresh should read all necessary attributes", 178 | { 179 | { 180 | channel = "device_lifecycle", 181 | direction = "receive", 182 | message = {mock_device.id, "added"} 183 | }, 184 | { 185 | channel = "capability", 186 | direction = "receive", 187 | message = { 188 | mock_device.id, 189 | { capability = "refresh", component = "main", command = "refresh", args = {} } 190 | } 191 | }, 192 | { 193 | channel = "zigbee", 194 | direction = "send", 195 | message = { 196 | mock_device.id, 197 | PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 198 | } 199 | }, 200 | { 201 | channel = "zigbee", 202 | direction = "send", 203 | message = { 204 | mock_device.id, 205 | PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) 206 | } 207 | }, 208 | { 209 | channel = "zigbee", 210 | direction = "send", 211 | message = { 212 | mock_device.id, 213 | TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) 214 | } 215 | }, 216 | { 217 | channel = "zigbee", 218 | direction = "send", 219 | message = { 220 | mock_device.id, 221 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Z_AXIS_ATTR_ID}, MFG_CODE) 222 | } 223 | }, 224 | { 225 | channel = "zigbee", 226 | direction = "send", 227 | message = { 228 | mock_device.id, 229 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {X_AXIS_ATTR_ID}, MFG_CODE) 230 | } 231 | }, 232 | { 233 | channel = "zigbee", 234 | direction = "send", 235 | message = { 236 | mock_device.id, 237 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {Y_AXIS_ATTR_ID}, MFG_CODE) 238 | } 239 | }, 240 | { 241 | channel = "zigbee", 242 | direction = "send", 243 | message = { 244 | mock_device.id, 245 | zigbee_test_utils.build_attribute_read(mock_device, MFG_CLUSTER, {ACCELERATION_ATTR_ID}, MFG_CODE) 246 | } 247 | }, 248 | { 249 | channel = "zigbee", 250 | direction = "send", 251 | message = { 252 | mock_device.id, 253 | IASZone.attributes.ZoneStatus:read(mock_device) 254 | } 255 | }, 256 | }, 257 | { 258 | inner_block_ordering = "relaxed" 259 | } 260 | ) 261 | 262 | test.register_coroutine_test( 263 | "Handle tempOffset preference in infochanged", 264 | function() 265 | test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) 266 | test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({preferences = {tempOffset = -5}})) 267 | test.wait_for_events() 268 | test.socket.zigbee:__queue_receive( 269 | { 270 | mock_device.id, 271 | TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) 272 | } 273 | ) 274 | test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) 275 | test.wait_for_events() 276 | end 277 | ) 278 | 279 | test.register_coroutine_test( 280 | "When set as a garage sensor, z-axis events should trigger contact events", 281 | function() 282 | test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({preferences = {useOnGarageDoor = "Yes"}})) 283 | test.wait_for_events() 284 | test.socket.zigbee:__queue_receive({mock_device.id, zigbee_test_utils.build_attribute_report(mock_device, MFG_CLUSTER, {{Y_AXIS_ATTR_ID, data_types.Int16.ID, 90}}, MFG_CODE) }) 285 | test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) 286 | end 287 | ) 288 | 289 | test.register_coroutine_test( 290 | "When set as a garage sensor, ias zone events should not trigger contact events", 291 | function() 292 | test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({preferences = {useOnGarageDoor = "Yes"}})) 293 | test.wait_for_events() 294 | test.socket.zigbee:__queue_receive({mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) }) 295 | end 296 | ) 297 | 298 | test.register_coroutine_test( 299 | "When not set as a garage sensor, ias zone events should trigger contact events", 300 | function() 301 | test.socket.zigbee:__queue_receive({mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) }) 302 | test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) 303 | end 304 | ) 305 | 306 | test.run_registered_tests() 307 | -------------------------------------------------------------------------------- /thingsim/README.md: -------------------------------------------------------------------------------- 1 | # thingsim 2 | 3 | These drivers are for use with the [`thingsim` device simulator]( 4 | https://github.com/SmartThingsCommunity/thingsim/). 5 | 6 | Each folder is a driver that supports a different protocol that is simulated by 7 | thingsim. 8 | 9 | * rpcclient - A client to thingsim devices' RPC servers. [[Tutorial]( 10 | https://community.smartthings.com/t/writing-an-rpc-client-edge-device-driver/230285)] 11 | * more to come, let us know what protocols you want to see 12 | -------------------------------------------------------------------------------- /thingsim/rpcclient/config.yml: -------------------------------------------------------------------------------- 1 | name: 'ThingSim RPC Client' 2 | packageKey: 'com.smartthings.thingsim.rpcclient' 3 | permissions: 4 | lan: {} 5 | discovery: {} 6 | -------------------------------------------------------------------------------- /thingsim/rpcclient/profiles/onoff.yaml: -------------------------------------------------------------------------------- 1 | name: thingsim.onoff.v1 2 | components: 3 | - id: main 4 | capabilities: 5 | - id: switch 6 | version: 1 7 | categories: 8 | - name: Light 9 | -------------------------------------------------------------------------------- /thingsim/rpcclient/src/discovery.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021 SmartThings 2 | -- 3 | -- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | -- in compliance with the License. You may obtain a copy of the License at: 5 | -- 6 | -- http://www.apache.org/licenses/LICENSE-2.0 7 | -- 8 | -- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | -- on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | -- for the specific language governing permissions and limitations under the License. 11 | 12 | local socket = require("socket") 13 | local log = require("log") 14 | 15 | -------------------------------------------------------------------------------------------- 16 | -- ThingSim device discovery 17 | -------------------------------------------------------------------------------------------- 18 | 19 | local looking_for_all = setmetatable({}, {__index = function() return true end}) 20 | 21 | local function process_response(val) 22 | local info = {} 23 | val = string.gsub(val, "HTTP/1.1 200 OK\r\n", "", 1) 24 | for k, v in string.gmatch(val, "([%g]+): ([%g ]*)\r\n") do 25 | info[string.lower(k)] = v 26 | end 27 | return info 28 | end 29 | 30 | local function device_discovery_metadata_generator(thing_ids, callback) 31 | local looking_for = {} 32 | local number_looking_for 33 | local number_found = 0 34 | if thing_ids ~= nil then 35 | number_looking_for = #thing_ids 36 | for _, id in ipairs(thing_ids) do looking_for[id] = true end 37 | else 38 | looking_for = looking_for_all 39 | number_looking_for = math.maxinteger 40 | end 41 | 42 | local s = socket.udp() 43 | assert(s) 44 | local listen_ip = interface or "0.0.0.0" 45 | local listen_port = 0 46 | 47 | local multicast_ip = "239.255.255.250" 48 | local multicast_port = 1900 49 | local multicast_msg = 50 | 'M-SEARCH * HTTP/1.1\r\n' .. 51 | 'HOST: 239.255.255.250:1982\r\n' .. 52 | 'MAN: "ssdp:discover"\r\n' .. 53 | 'MX: 1\r\n' .. 54 | 'ST: urn:smartthings-com:device:thingsim:1\r\n' 55 | 56 | -- Create bind local ip and port 57 | -- simulator will unicast back to this ip and port 58 | assert(s:setsockname(listen_ip, listen_port)) 59 | local timeouttime = socket.gettime() + 8 60 | s:settimeout(8) 61 | 62 | local ids_found = {} -- used to filter duplicates 63 | assert(s:sendto(multicast_msg, multicast_ip, multicast_port)) 64 | while number_found < number_looking_for do 65 | local time_remaining = math.max(0, timeouttime-socket.gettime()) 66 | s:settimeout(time_remaining) 67 | local val, rip, rport = s:receivefrom() 68 | if val then 69 | log.trace(val) 70 | local headers = process_response(val) 71 | local ip, port = headers["location"]:match("http://([^,]+):([^/]+)") 72 | local rpcip, rpcport = (headers["rpc.smartthings.com"] or ""):match("rpc://([^,]+):([^/]+)") 73 | local httpip, httpport = (headers["http.smartthings.com"] or ""):match("http://([^,]+):([^/]+)") 74 | local id = headers["usn"]:match("uuid:([^:]+)") 75 | local name = headers["name.smartthings.com"] 76 | 77 | if rip ~= ip then 78 | log.warn("recieved discovery response with reported & source IP mismatch, ignoring") 79 | elseif ip and port and id and looking_for[id] and not ids_found[id] then 80 | ids_found[id] = true 81 | number_found = number_found + 1 82 | -- TODO: figure out if it's possible to make recursive coroutines work inside cosock 83 | --coroutine.yield({ip = ip, port = port, info = info}) 84 | callback({id = id, ip = ip, port = port, rpcport = rpcport, httpport = httpport, name = name}) 85 | else 86 | log.debug("found device not looking for:", id) 87 | end 88 | elseif rip == "timeout" then 89 | return nil 90 | else 91 | error(string.format("error receving discovery replies: %s", rip)) 92 | end 93 | end 94 | end 95 | 96 | local function find_cb(thing_ids, cb) 97 | device_discovery_metadata_generator(thing_ids, cb) 98 | end 99 | 100 | local function find(thing_ids) 101 | local thingsmeta = {} 102 | local function cb(metadata) table.insert(thingsmeta, metadata) end 103 | find_cb(thing_ids, cb) 104 | return thingsmeta 105 | end 106 | 107 | 108 | return { 109 | find = find, 110 | find_cb = find_cb, 111 | } 112 | -------------------------------------------------------------------------------- /thingsim/rpcclient/src/init.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021 SmartThings 2 | -- 3 | -- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | -- in compliance with the License. You may obtain a copy of the License at: 5 | -- 6 | -- http://www.apache.org/licenses/LICENSE-2.0 7 | -- 8 | -- Unless required by applicable law or agreed to in writing, software distributed under the 9 | -- License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 10 | -- either express or implied. See the License for the specific language governing permissions and 11 | -- limitations under the License. 12 | -- 13 | 14 | -- smartthings libraries 15 | local capabilities = require "st.capabilities" 16 | local Driver = require "st.driver" 17 | local log = require "log" 18 | 19 | -- the coroutine runtime's socket interface 20 | local cosock = require "cosock" 21 | 22 | -- driver specific libraries from this repo 23 | local client = cosock.asyncify "rpcclient" 24 | local discovery = cosock.asyncify "discovery" 25 | 26 | ---------------------------------------------------------------------------------------------------- 27 | -- Local Helpers 28 | ---------------------------------------------------------------------------------------------------- 29 | 30 | -- search network for specific thing using custom discovery library 31 | local function find_thing(id) 32 | -- for the sake of brevity, this currently sends out an SSDP broadcast for each thing, though you 33 | -- should coalesce these to just send out a single request for the whole driver 34 | return table.remove(discovery.find({id}) or {}) 35 | end 36 | 37 | -- get an rpc client for thing if thing is reachable on the network 38 | local function get_thing_client(device) 39 | local thingclient = device:get_field("client") 40 | 41 | if not thingclient then 42 | local thing = find_thing(device.device_network_id) 43 | if thing then 44 | thingclient = client.new(thing.ip, thing.rpcport) 45 | device:set_field("client", thingclient) 46 | device:online() 47 | end 48 | end 49 | 50 | if not thingclient then 51 | device:offline() 52 | return nil, "unable to reach thing" 53 | end 54 | 55 | return thingclient 56 | end 57 | 58 | ---------------------------------------------------------------------------------------------------- 59 | -- Device and Driver Event Handlers 60 | ---------------------------------------------------------------------------------------------------- 61 | 62 | -- shared helper for emitting the initial state of a device when 63 | -- either added fresh or when the driver starts up 64 | local function initialize_device_state(device) 65 | log.info("[" .. tostring(device.id) .. "] Initializing ThingSim RPC Client device") 66 | 67 | local client = assert(get_thing_client(device)) 68 | 69 | if client then 70 | log.info("Connected") 71 | 72 | -- get current state and emit in case it has changed 73 | local attrs = client:getattr({"power"}) 74 | if attrs and attrs.power == "on" then 75 | device:emit_event(capabilities.switch.switch.on()) 76 | else 77 | device:emit_event(capabilities.switch.switch.off()) 78 | end 79 | else 80 | log.warn( 81 | "Device not found at initial discovery (no async events until controlled)", 82 | device:get_field("name") or device.device_network_id 83 | ) 84 | end 85 | end 86 | 87 | -- initialize device at startup 88 | local function device_init(driver, device) 89 | initialize_device_state(device) 90 | end 91 | 92 | -- initialize device when added 93 | local function device_added(driver, device) 94 | initialize_device_state(device) 95 | end 96 | 97 | -- discover not already known devices listening on the network 98 | local function discovery_handler(driver, options, should_continue) 99 | log.info("starting discovery") 100 | local known_devices = {} 101 | local found_devices = {} 102 | 103 | -- get a list of devices already added 104 | local device_list = driver:get_devices() 105 | for i, device in ipairs(device_list) do 106 | -- for each, add to a table keyed by the the DNI for easy lookup later 107 | local id = device.device_network_id 108 | known_devices[id] = true 109 | end 110 | 111 | -- as long as a user is on the device discovery page in the app, calling `should_continue()` 112 | -- will return `true` and we should keep trying to discover more thingsim devices 113 | while should_continue() do 114 | log.info("making discovery request") 115 | discovery.find_cb( 116 | nil, -- find all things 117 | function(device) 118 | -- handle when any (known or new) device responds 119 | local id = device.id 120 | local ip = device.ip 121 | local name = device.name or "Unnamed ThingSim RPC Client" 122 | 123 | -- but only add if we didn't already know about it and haven't just found it in a prev loop 124 | if not known_devices[id] and not found_devices[id] then 125 | found_devices[id] = true 126 | log.info(string.format("adding %s at %s", name or id, ip)) 127 | assert( 128 | driver:try_create_device({ 129 | type = "LAN", 130 | device_network_id = id, 131 | label = name, 132 | profile = "thingsim.onoff.v1", 133 | manufacturer = "thingsim", 134 | model = "On/Off Bulb", 135 | vendor_provided_name = name 136 | }), 137 | "failed to send found_device" 138 | ) 139 | end 140 | end 141 | ) 142 | end 143 | log.info("exiting discovery") 144 | end 145 | 146 | ---------------------------------------------------------------------------------------------------- 147 | -- Command Handlers 148 | ---------------------------------------------------------------------------------------------------- 149 | 150 | function handle_on(driver, device, command) 151 | log.info("switch on", device.id) 152 | 153 | local client = assert(get_thing_client(device)) 154 | if client:setattr{power = "on"} then 155 | device:emit_event(capabilities.switch.switch.on()) 156 | else 157 | log.error("failed to set power on") 158 | end 159 | end 160 | 161 | function handle_off(driver, device, command) 162 | log.info("switch off", device.id) 163 | 164 | local client = assert(get_thing_client(device)) 165 | if client:setattr{power = "off"} then 166 | device:emit_event(capabilities.switch.switch.off()) 167 | else 168 | log.error("failed to set power on") 169 | end 170 | end 171 | 172 | ---------------------------------------------------------------------------------------------------- 173 | -- Build and Run Driver 174 | ---------------------------------------------------------------------------------------------------- 175 | 176 | local rpc_client_driver = Driver("rpc client", 177 | { 178 | discovery = discovery_handler, 179 | lifecycle_handlers = { 180 | added = device_added, 181 | init = device_init, 182 | }, 183 | capability_handlers = { 184 | [capabilities.switch.ID] = { 185 | [capabilities.switch.commands.on.NAME] = handle_on, 186 | [capabilities.switch.commands.off.NAME] = handle_off 187 | }, 188 | } 189 | } 190 | ) 191 | 192 | rpc_client_driver.bulb_handles = {} 193 | 194 | rpc_client_driver:run() 195 | -------------------------------------------------------------------------------- /thingsim/rpcclient/src/rpcclient.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021 SmartThings 2 | -- 3 | -- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | -- in compliance with the License. You may obtain a copy of the License at: 5 | -- 6 | -- http://www.apache.org/licenses/LICENSE-2.0 7 | -- 8 | -- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 9 | -- on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 10 | -- for the specific language governing permissions and limitations under the License. 11 | local socket = require 'socket' 12 | local json = require 'dkjson' 13 | 14 | local client = {} 15 | 16 | -- internal function that actually performs the RPC on the network 17 | -- note: by convention, a leading underscore in a name means something internal 18 | function client:_call(method, ...) 19 | -- new request, new id 20 | self.last_req_id = self.last_req_id + 1 21 | 22 | -- structure call with a table 23 | local request = { 24 | id = self.last_req_id, 25 | method = method, 26 | params = {...} 27 | } 28 | 29 | -- encode the call as a json-formatted string 30 | local requeststr = assert(json.encode(request)) 31 | 32 | -- send our encoded request, terminated by a newline character 33 | local bytessent, err = self.sock:send(requeststr.."\n") 34 | assert(bytessent, "failed to send request") 35 | assert(bytessent == #requeststr + 1, "request only partially sent") 36 | 37 | while true do 38 | -- by default `receive` reads a line of data, perfect for our protocol 39 | local line, err = self.sock:receive() 40 | assert(line, "failed to get response:" .. tostring(err)) 41 | 42 | -- decode the response into a lua table 43 | local resp, cont, err = json.decode(line) 44 | assert(resp, "failed to parse response") 45 | 46 | if resp.id then 47 | assert(resp.id == request.id, "unexpected response") 48 | 49 | -- return the result of the call back to the caller 50 | return table.unpack(resp.result) 51 | else 52 | -- a "resp" without an id is a notification, ignore for now 53 | -- and let the loop take us back around to try again 54 | end 55 | end 56 | end 57 | 58 | function client.new(ip, port) 59 | local sock = socket.tcp() 60 | 61 | -- in a real world driver you'll probably want more reliable connect logic than this 62 | assert(sock:connect(ip, port)) 63 | 64 | local o = { sock = sock, ip = ip, port = port, last_req_id = 0 } 65 | setmetatable(o, {__index = client}) 66 | return o 67 | end 68 | 69 | -- `setattr` RPC 70 | -- 71 | -- sets attributes on the thing 72 | function client:setattr(attrmap) 73 | return self:_call("setattr", attrmap) 74 | end 75 | 76 | -- `getattr` RPC 77 | -- 78 | -- gets the current value of attributes of thing 79 | function client:getattr(attrlist) 80 | return self:_call("getattr", attrlist) 81 | end 82 | 83 | return client 84 | --------------------------------------------------------------------------------