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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions driver_name_cap/capabilities/driverName-presentation.yaml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions driver_name_cap/capabilities/driverName.yaml
Original file line number Diff line number Diff line change
@@ -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: {}
5 changes: 5 additions & 0 deletions driver_name_cap/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: driver-name
packageKey: driver-name
permissions:
lan: {}
discovery: {}
6 changes: 6 additions & 0 deletions driver_name_cap/profiles/driver_name-profile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: driver_name.v1
components:
- id: main
capabilities:
- id: honestadmin11679.driverName
version: 1
172 changes: 172 additions & 0 deletions driver_name_cap/readme.md
Original file line number Diff line number Diff line change
@@ -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=<hub-ip>
┌───┬──────────────────────────────────────┬─────────────┐
│ | 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://<hub-ip>: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
```
88 changes: 88 additions & 0 deletions driver_name_cap/src/disco.lua
Original file line number Diff line number Diff line change
@@ -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,
}
42 changes: 42 additions & 0 deletions driver_name_cap/src/init.lua
Original file line number Diff line number Diff line change
@@ -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()