diff --git a/driver_name_cap/capabilities/driverName-presentation.yaml b/driver_name_cap/capabilities/driverName-presentation.yaml new file mode 100644 index 0000000..349d8e1 --- /dev/null +++ b/driver_name_cap/capabilities/driverName-presentation.yaml @@ -0,0 +1,12 @@ +dashboard: + states: + - label: '{{driverName.value}}' + actions: [] + basicPlus: [] +detailView: + - label: Driver Name + displayType: state + state: + label: '{{driverName.value}}' +id: honestadmin11679.driverName +version: 1 diff --git a/driver_name_cap/capabilities/driverName.yaml b/driver_name_cap/capabilities/driverName.yaml new file mode 100644 index 0000000..d0a6324 --- /dev/null +++ b/driver_name_cap/capabilities/driverName.yaml @@ -0,0 +1,16 @@ +id: honestadmin11679.driverName +version: 1 +status: proposed +name: Driver Name +attributes: + currentUrl: + schema: + type: object + properties: + value: + type: string + additionalProperties: false + required: + - value + enumCommands: [] +commands: {} diff --git a/driver_name_cap/config.yml b/driver_name_cap/config.yml new file mode 100644 index 0000000..f39a6ab --- /dev/null +++ b/driver_name_cap/config.yml @@ -0,0 +1,5 @@ +name: driver-name +packageKey: driver-name +permissions: + lan: {} + discovery: {} diff --git a/driver_name_cap/profiles/driver_name-profile.yml b/driver_name_cap/profiles/driver_name-profile.yml new file mode 100644 index 0000000..31f1e60 --- /dev/null +++ b/driver_name_cap/profiles/driver_name-profile.yml @@ -0,0 +1,6 @@ +name: driver_name.v1 +components: +- id: main + capabilities: + - id: honestadmin11679.driverName + version: 1 diff --git a/driver_name_cap/readme.md b/driver_name_cap/readme.md new file mode 100644 index 0000000..91a7bb4 --- /dev/null +++ b/driver_name_cap/readme.md @@ -0,0 +1,172 @@ +# Driver Name Cap + +This driver emulates a button device by listening for http POST requests on the hub. + +To interact with the driver you simply need to `curl` the correct ip and port combination. + +## Getting started + +### Adding your first device + +After installing this driver to your hub, you will need to add the first device from OneApp. +To do so, you will select the + button that appears in the top right of the main user interface. +This will bring you to a list of things you could add to a location, from this list select the +"Device" option. This will bring you to a list of possible device types you could add, instead +of selecting one of those, there is a button on this screen that says "Scan Nearby" (On iOS this +is in the bottom right, on Android, it is in the top right). This will start running your newly +installed `http_button` driver which will discover a single device. + +## Getting the port + +The driver isn't allowed to request a port when opening a socket this means the port will +change each time this driver is started. To determine what port you need to interact with +you have 3 options + +### The Smartthings App + +This driver emits a capability "Current URL" which will display the full URL for the server +in the detail view of every button device. Simply select the device card from the app and +it should populate. + +### Driver Logs + +Start the live logs from `smartthings` CLI and a log message coming from `http_button` will +pop up every minute with the IP and port number in them. + +```sh +smartthings edge:drivers:logcat --hub-address= +┌───┬──────────────────────────────────────┬─────────────┐ +│ | Driver Id │ Name │ +├───┼──────────────────────────────────────┼─────────────┤ +│ 1 │ b2ddeeba-a895-4457-9584-bb23a990cb78 │ http_button │ +└───┴──────────────────────────────────────┴─────────────┘ +? Select a driver. (all) 1 +connecting... connected +2022-06-01T19:36:12.784002782+00:00 INFO http_button listening on http://192.168.1.6:35983 +``` + +### UDP Broadcast + +If you are planning on interacting with this driver programmatically, you can +send a UDP broadcast message to the ip 239.255.255.250 on port 9887. In Lua that might look +like this + +```lua +local cosock = require "cosock" + +local function find_url() + local ip = '239.255.255.250' + local port = 9887 + local sock = cosock.socket.udp() + assert(sock:setsockname(ip, port)) + -- Ensure we can reuse our port number + assert(sock:setoption('reuseaddr', true)) + -- Add ourselves to the broadcast group + assert(sock:setoption('ip-add-membership', {multiaddr = ip, interface = '0.0.0.0'})) + -- Ignore the messages we send ourselves + assert(sock:setoption('ip-multicast-loop', false)) + -- Don't wait longer than 5 seconds for a reply + sock:settimeout(5) + while true do + -- Send the query to the broadcast group + sock:sendto("whereareyou", ip, port) + print("sent whereareyou") + while true do + -- Listen for a reply from the group + url, ip_or_err, _port = sock:receivefrom() + + print("received", url, ip_or_err, _port) + if url and url:match("^http://") then + return url + else + print("Error: ", url, ip_or_err, _port) + break + end + end + end +end + +cosock.spawn(find_url) +cosock.run() + +``` + +## Interacting with the driver + +Once you have your IP/port number, you can use `curl` or `wget` or `postman` to trigger +button presses. The following examples will be using `curl` but the basic idea is +that you need to send an HTTP POST requests to the IP and port you captured above +followed by the device id or special endpoint. + +An alternative is to head to `http://:port/index.html` in your browser for GUI interactions. + +### Examples + +These will assume an IP address of `192.168.0.199` and a port of `54345`. + +#### Get the device ids + +```sh +$ curl -X GET http://192.168.0.199:54345/info +[{"device_id": "aaaaaaaa-bbbb-cccc-dddd-ffffffffffff", "device_name": "button 0"} +{"device_id": "11111111-2222-3333-4444-555555555555", "device_name": "button 1"}] +``` + +This would indicate that there are 2 buttons we can interact with. + +#### Triggering a push event + +```sh +$ curl -X POST http://192.168.0.199:54345/action -H "Content-Type: application/json" \ +-d "{\"device_id\": \"aaaaaaaa-bbbb-cccc-dddd-ffffffffffff\", \"action\": \"push\" }" +``` + +#### Triggering a held event + +```sh +$ curl -X POST http://192.168.0.199:54345/action -H "Content-Type: application/json" \ +-d "{\"device_id\": \"aaaaaaaa-bbbb-cccc-dddd-ffffffffffff\", \"action\": \"hold\" }" +``` + +#### Adding Devices + +[The first device you add to this _has_ to come through via OneApp.](#getting-started) + +After the first button, the driver will do nothing when you "Scan Nearby" instead you will want +to use the `/newdevice` endpoint + +```sh +curl -X POST http://192.168.0.199:54345/newdevice +``` + +#### Updating Devices + +> This one will take a long time to sync up across the platform at the moment + +Once you have a few devices installed, if you wanted to change their label, you can do so with the following. + +```sh +curl -X POST http://192.168.0.199:54345/newlabel -H "Content-Type: application/json" \ +-d "{\"device_id\": \"aaaaaaaa-bbbb-cccc-dddd-ffffffffffff\", \"name\": \"Party Button!!!\" }"" +``` + +#### Quieting the ping message + +At startup, the driver will start sending a log message with the current IP address and the port +that was provided by the hub every 5 seconds. To stop this message you can send a request to `/quiet` + +```sh +curl -X POST http://192.168.0.199:54345/quiet +Stopped ping loop +``` + +#### Checking if the server is running + +It may be nice to just check if the server is running, to do this the `/health` endpoint exists for +that. Any request to this will either return `1` if the server is up or fail if the +server is down + +```sh +curl http://192.168.0.199:54345/health +1 +``` diff --git a/driver_name_cap/src/disco.lua b/driver_name_cap/src/disco.lua new file mode 100644 index 0000000..324f2b4 --- /dev/null +++ b/driver_name_cap/src/disco.lua @@ -0,0 +1,88 @@ +local json = require 'dkjson' +local log = require 'log' + +local function generate_block(size) + local hex_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} + local ret = '' + for _ = 1,size,1 do + ret = ret .. hex_digits[math.random(1, 16)] + end + if size < 12 then + ret = ret .. '-' + end + return ret +end + +local function new_uuid() + return generate_block(8) .. + generate_block(4) .. + generate_block(4) .. + generate_block(4) .. + generate_block(12) +end + +--- Add a new device to this driver +--- +---@param driver Driver The driver instance to use +---@param device_number number|nil If populated this will be used to generate the device name/label if not, `get_device_list` +--- will be called to provide this value +local function add_device(driver, device_number) + log.trace('add_device') + if device_number == nil then + log.debug('determining current device count') + local device_list = driver.device_api.get_device_list() + device_number = #device_list + end + local device_name = 'Device ' .. device_number + log.debug('adding device ' .. device_name) + local device_id = new_uuid() + local device_info = { + type = 'LAN', + deviceNetworkId = device_id, + label = device_name, + profileReference = 'driver_name.v1', + vendorProvidedName = device_name, + } + local device_info_json = json.encode(device_info) + local success, msg = driver.device_api.create_device(device_info_json) + if success then + log.debug('successfully created device') + return device_name, device_id + end + log.error(string.format('unsuccessful create_device %s', msg)) + return nil, nil, msg +end + +--- A discovery pass that will discover exactly 1 device +--- for a driver. I any devices are already associated with +--- this driver, no devices will be discovered +--- +---@param driver Driver the driver name to use when discovering a device +---@param opts table the discovery options +---@param cont function function to check if discovery should continue +local function disco_handler(driver, opts, cont) + log.trace('disco') + + if cont() then + local device_list = driver.device_api.get_device_list() + log.trace('starting discovery') + if #device_list > 0 then + log.debug('stopping discovery with ' .. #device_list .. ' devices') + return + end + log.debug('Adding first ' .. driver.NAME .. ' device') + local device_name, device_id, err = add_device(driver, #device_list) + if err ~= nil then + log.error(err) + return + end + log.info('added new device ' .. device_name) + end +end + + + +return { + disco_handler = disco_handler, + add_device = add_device, +} diff --git a/driver_name_cap/src/init.lua b/driver_name_cap/src/init.lua new file mode 100644 index 0000000..0a199dc --- /dev/null +++ b/driver_name_cap/src/init.lua @@ -0,0 +1,42 @@ +local capabilities = require 'st.capabilities' +local Driver = require 'st.driver' +local log = require 'log' + +local discovery = require 'disco' +local utils = require 'st.utils' + +local driverNameID = "honestadmin11679.driverName" +local driverName = capabilities[driverNameID] + +-- These handlers are primarily to make the log traffic +-- as chatty as possible +local function device_added(driver, device) + device:emit_event(driverName.driverName(driver.NAME)) + log.trace('Added ' .. device.id) +end + +local function device_init(driver, device) + device:emit_event(driverName.driverName(driver.NAME)) + log.trace('Init\'d ' .. device.id) +end + +local function device_removed(driver, device) + log.trace('Removed ' .. device.id) +end + +local function info_changed(driver, device, event, ...) + log.trace('Info Changed ', device.id, event, ...) +end + +local driver = Driver('Driver Name', { + lifecycle_handlers = { + init = device_init, + added = device_added, + deleted = device_removed, + infoChanged = info_changed, + }, + discovery = discovery.disco_handler, +}) + + +driver:run()