LaMetric Time

Anyone have a LaMetric Time moved over to work on Hubitat? I have one on ST that I would love to be able to move over. but fixing the code is beyond my current ability, especially OAuth.

https://help.lametric.com/support/solutions/articles/6000183330-integrating-lametric-time-with-smartthings-step-by-step-

Edit: I did submit a request to LaMetric to create an integration with Hubitat.

1 Like

Iā€™m in the same boat.

DH ported:

/**
 *  LaMetric
 *
 *  Copyright 2016 Smart Atoms Ltd.
 *  Author: Mykola Kirichuk
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
metadata {
	definition (name: "LaMetric", namespace: "com.lametric", author: "Mykola Kirichuk") {
		capability "Actuator"
		capability "Notification"
		capability "Polling"
		capability "Refresh"
        
        attribute "currentIP", "string"
        attribute "serialNumber", "string"
        attribute "volume", "string"
        attribute "mode", "enum", ["offline","online"]
        
        command "setOffline"
        command "setOnline"
	}

	simulator {
		// TODO: define status and reply messages here
	}

	tiles (scale: 2){
		// TODO: define your main and details tiles here
         tiles(scale: 2) {
     	multiAttributeTile(name:"rich-control"){
			tileAttribute ("mode", key: "PRIMARY_CONTROL") {
	            attributeState "online", label: "LaMetric", action: "", icon:  "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3C200"
                attributeState "offline", label: "LaMetric", action: "", icon: "https://developer.lametric.com/assets/smart_things/time_100.png", backgroundColor: "#F3F3F3"
			}
	        tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
	            attributeState "default", label:'SN: ${currentValue}'
			}
        }
		valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
			state "default", label:'SN: ${currentValue}'
		}
		valueTile("networkAddress", "device.currentIP", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
			state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
		}

       		standardTile("presence", "device.mode", width: 2, height: 2, canChangeBackground: true) {
           		state "default", icon:"https://developer.lametric.com/assets/smart_things/time_100.png"
		}

		main (["presence"])
		details(["rich-control","networkAddress"])
	}
	}
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"
    if (description)
    {
    	unschedule("setOffline")
    }
	// TODO: handle 'battery' attribute
	// TODO: handle 'button' attribute
	// TODO: handle 'status' attribute
	// TODO: handle 'level' attribute
	// TODO: handle 'level' attribute

}

// handle commands
def setOnline()
{
	log.debug("set online");
    sendEvent(name:"mode", value:"online")
  	unschedule("setOffline")
}
def setOffline(){
	log.debug("set offline");
    sendEvent(name:"mode", value:"offline")
}

def setLevel(level) {
	log.debug "Executing 'setLevel' ${level}"
	// TODO: handle 'setLevel' command
}

def deviceNotification(notif) {
	log.debug "Executing 'deviceNotification' ${notif}"
	// TODO: handle 'deviceNotification' command
    def result = parent.sendNotificationMessageToDevice(device.deviceNetworkId, notif);
    log.debug ("result ${result}");
    log.debug parent;
    return result;
}

def poll() {
	// TODO: handle 'poll' command
    log.debug "Executing 'poll'"
  	if (device.currentValue("currentIP") != "Offline")
    {
	    runIn(30, setOffline)
    }
    parent.poll(device.deviceNetworkId)
}

def refresh() {    
	log.debug "Executing 'refresh'"
//    log.debug "${device?.currentIP}"
    log.debug "${device?.currentValue("currentIP")}"
    log.debug "${device?.currentValue("serialNumber")}"
    log.debug "${device?.currentValue("volume")}"
//    poll()
	
}

/*def setLevel() {
	log.debug "Executing 'setLevel'"
	// TODO: handle 'setLevel' command
}*/

Connect app:

/**
 *  LaMetric (Connect)
 *
 *  Copyright 2016 Smart Atoms Ltd.
 *  Author: Mykola Kirichuk
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
 
definition(
    name: "LaMetric (Connect)",
    namespace: "com.lametric",
    author: "Mykola Kirichuk",
    description: "Control your LaMetric Time smart display",
    category: "Family",
    iconUrl: "https://developer.lametric.com/assets/smart_things/smart_things_60.png",
    iconX2Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
    iconX3Url: "https://developer.lametric.com/assets/smart_things/smart_things_120.png",
    singleInstance: true)
{
    appSetting "clientId"
    appSetting "clientSecret"
}

preferences {
    page(name: "auth", title: "LaMetric", nextPage:"", content:"authPage", uninstall: true, install:true)
    page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5);
}

mappings {
    path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
    path("/oauth/callback") {action: [GET: "callback"]}
}

import groovy.json.JsonOutput

def getEventNameListOfUserDeviceParsed(){ "EventListOfUserRemoteDevicesParsed" }
def getEventNameTokenRefreshed(){ "EventAuthTokenRefreshed" }

def installed() {
    log.debug "Installed with settings: ${settings}"
    initialize()
}

def updated() {
    log.debug "Updated with settings: ${settings}"
    sendEvent(name:"Updated", value:true)
    unsubscribe()
    initialize()


}

def initialize() {
    // TODO: subscribe to attributes, devices, locations, etc.
    log.debug("initialize");
    state.subscribe  = false;
    if (selecteddevice) {
        addDevice()
        subscribeNetworkEvents(true)
        refreshDevices();
    }
}

/**
* Get the name of the new device to instantiate in the user's smartapps
* This must be an app owned by the namespace (see #getNameSpace).
*
* @return name
*/

def getDeviceName() {
    return "LaMetric"
}

/**
* Returns the namespace this app and siblings use
*
* @return namespace
*/
def getNameSpace() {
    return "com.lametric"
}


/**
* Returns all discovered devices or an empty array if none
*
* @return array of devices
*/
def getDevices() {
    state.remoteDevices = state.remoteDevices ?: [:]
}

/**
* Returns an array of devices which have been verified
*
* @return array of verified devices
*/
def getVerifiedDevices() {
    getDevices().findAll{ it?.value?.verified == true }
}

/**
* Generates a Map object which can be used with a preference page
* to represent a list of devices detected and verified.
*
* @return Map with zero or more devices
*/
Map getSelectableDevice() {
    def devices = getVerifiedDevices()
    def map = [:]
    devices.each {
        def value = "${it.value.name}"
        def key = it.value.id
        map["${key}"] = value
    }
    map
}

/**
* Starts the refresh loop, making sure to keep us up-to-date with changes
*
*/
private refreshDevices(){
    log.debug "refresh device list"
    listOfUserRemoteDevices()
    //every 30 min
    runIn(1800, "refreshDevices")
}

/**
* The deviceDiscovery page used by preferences. Will automatically
* make calls to the underlying discovery mechanisms as well as update
* whenever new devices are discovered AND verified.
*
* @return a dynamicPage() object
*/
/******************************************************************************************************************
DEVICE DISCOVERY AND VALIDATION
******************************************************************************************************************/
def deviceDiscovery()
{
    //    if(canInstallLabs())
    if (1)
    {
        //    	userDeviceList();
        log.debug("deviceDiscovery")
        def refreshInterval = 3 // Number of seconds between refresh
        int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
            state.deviceRefreshCount = deviceRefreshCount + refreshInterval

        def devices = getSelectableDevice()
        def numFound = devices.size() ?: 0

        // Make sure we get location updates (contains LAN data such as SSDP results, etc)
        subscribeNetworkEvents()

        //device discovery request every 15s
        //        if((deviceRefreshCount % 15) == 0) {
        //            discoverLaMetrics()
        //        }

        // Verify request every 3 seconds except on discoveries
        if(((deviceRefreshCount % 5) == 0)) {
            verifyDevices()
        }

        log.trace "Discovered devices: ${devices}"

        return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
            section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
                input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
            }
        }
    }
    else
    {
        def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.

To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""

        return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
            section("Upgrade") {
                paragraph "$upgradeNeeded"
            }
        }
    }
}

/**

/**
* Starts a subscription for network events
*
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
*/
private subscribeNetworkEvents(force=false) {
    if (force) {
        unsubscribe()
        state.subscribe = false
    }
    if(!state.subscribe) {
        log.debug("subscribe on network events")
        subscribe(location, null, locationHandler, [filterEvents:false])
        //        subscribe(app, appHandler)
        state.subscribe = true
    }
}

private verifyDevices()
{
    log.debug "verify.devices"
    def devices = getDevices();
    for (it in devices) {
        log.trace ("verify device ${it.value}")
        def localIp = it?.value?.ipv4_internal;
        def apiKey = it?.value?.api_key;
        getAllInfoFromDevice(localIp, apiKey);
    }
}
def appHandler(evt)
{
    log.debug("application event handler ${evt.name}")
    if (evt.name == eventNameListOfUserDeviceParsed)
    {
        log.debug ("new account device list received ${evt.value}")
        def newRemoteDeviceList
        try {
            newRemoteDeviceList = parseJson(evt.value)
        } catch (e)
        {
            log.debug "Wrong value ${e}"
        }
        if (newRemoteDeviceList)
        {
            def remoteDevices = getDevices();
            newRemoteDeviceList.each{deviceInfo ->
                if (deviceInfo) {
                    def device = remoteDevices[deviceInfo.id]?:[:];
                        log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
                    deviceInfo.each() {
                        device[it.key] = it.value;
                    }
                    remoteDevices[deviceInfo.id] = device;
                } else {
                    log.debug ("empty device info")
                }
            }
            verifyDevices();
        } else {
            log.debug "wrong value ${newRemoteDeviceList}"
        }
    } else if (evt.name == getEventNameTokenRefreshed())
    {
        log.debug "token refreshed"
        state.refreshToken = evt.refreshToken
        state.authToken = evt.access_token
    }
}

def locationHandler(evt)
{
    log.debug("network event handler ${evt.name}")
    if (evt.name == "ssdpTerm")
    {
        log.debug "ignore ssdp"
    } else {
        def lanEvent = parseLanMessage(evt.description, true)
        log.debug lanEvent.headers;
        if (lanEvent.body)
        {
            log.trace "lan event ${lanEvent}";
            def parsedJsonBody;
            try {
                parsedJsonBody = parseJson(lanEvent.body);
            } catch (e)
            {
                log.debug ("not json responce ignore $e");
            }
            if (parsedJsonBody)
            {
                log.trace (parsedJsonBody)
                log.debug("responce for device ${parsedJsonBody?.id}")
                //put or post response
                if (parsedJsonBody.success)
                {

                } else {
                    //poll response
                    log.debug "poll responce"
                    log.debug ("poll responce ${parsedJsonBody}")
                    def deviceId = parsedJsonBody?.id;
                    if (deviceId)
                    {
                        def devices = getDevices();
                        def device = devices."${deviceId}";

                        device.verified = true;
                        device.dni = [device.serial_number, device.id].join('.')
                        device.hub = evt?.hubId;
                        device.volume = parsedJsonBody?.audio?.volume;
                        log.debug "verified device ${deviceId}"
                        def childDevice = getChildDevice(device.dni)
                        //update device info
                        if (childDevice)
                        {
                            log.debug("send event to ${childDevice}")
                            childDevice.sendEvent(name:"currentIP",value:device?.ipv4_internal);
                            childDevice.sendEvent(name:"volume",value:device?.volume);
                            childDevice.setOnline();
                        }
                        log.trace device
                    }
                }
            }
        }
    }
}

/**
* Adds the child devices based on the user's selection
*
* Uses selecteddevice defined in the deviceDiscovery() page
*/
def addDevice() {
    def devices = getVerifiedDevices()
    def devlist
    log.trace "Adding childs"

    // If only one device is selected, we don't get a list (when using simulator)
    if (!(selecteddevice instanceof List)) {
        devlist = [selecteddevice]
    } else {
        devlist = selecteddevice
    }

    log.trace "These are being installed: ${devlist}"
    log.debug ("devlist" +  devlist)
    devlist.each { dni ->
        def newDevice = devices[dni];
        if (newDevice)
        {
            def d = getChildDevice(newDevice.dni)
            if(!d) {
                log.debug ("get child devices"  + getChildDevices())
                log.trace "concrete device ${newDevice}"
                def deviceName = newDevice.name
                d = addChildDevice(getNameSpace(), getDeviceName(), newDevice.dni, newDevice.hub, [label:"${deviceName}"])
                def childDevice = getChildDevice(d.deviceNetworkId)
                childDevice.sendEvent(name:"serialNumber", value:newDevice.serial_number)
                log.trace "Created ${d.displayName} with id $dni"
            } else {
                log.trace "${d.displayName} with id $dni already exists"
            }
        }
    }
}


//******************************************************************************************************************
//		 					 					 			OAUTH
//******************************************************************************************************************

def getServerUrl()           		{ "https://graph.api.smartthings.com" }
def getShardUrl()            		{ getApiServerUrl() }
def getCallbackUrl()        		{ "https://graph.api.smartthings.com/oauth/callback" }
def getBuildRedirectUrl()   		{ "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
def getApiEndpoint()        		{ "https://developer.lametric.com" }
def getTokenUrl()					{ "${apiEndpoint}${apiTokenPath}" }
def getAuthScope() 					{ [ "basic", "devices_read" ] }
def getSmartThingsClientId() 		{ appSettings.clientId }
def getSmartThingsClientSecret() 	{ appSettings.clientSecret }
def getApiTokenPath()				{ "/api/v2/oauth2/token" }
def getApiUserMeDevicesList()		{ "/api/v2/users/me/devices" }

def toQueryString(Map m) {
    return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
def composeScope(List scopes)
{
    def result = "";
    scopes.each(){ scope ->
        result += "${scope} "
    }
    if (result.length())
    return result.substring(0, result.length() - 1);
    return "";
}

def authPage() {
    log.debug "authPage()"

    if(!state.accessToken) { //this is to access token for 3rd party to make a call to connect app
        state.accessToken = createAccessToken()
    }

    def description
    def uninstallAllowed = false
    def oauthTokenProvided = false

    if(state.authToken) {
        description = "You are connected."
        uninstallAllowed = true
        oauthTokenProvided = true
    } else {
        description = "Click to enter LaMetric Credentials"
    }

    def redirectUrl = buildRedirectUrl
    log.debug "RedirectUrl = ${redirectUrl}"
    // get rid of next button until the user is actually auth'd
    if (!oauthTokenProvided) {
        return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
            section(){
                paragraph "Tap below to log in to the LaMatric service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
                href url:redirectUrl, style:"embedded", required:true, title:"LaMetric", description:description
            }
        }
    } else {
        subscribeNetworkEvents()
        listOfUserRemoteDevices()
        return deviceDiscovery();
    }
}


private refreshAuthToken() {
    log.debug "refreshing auth token"

    if(!state.refreshToken) {
        log.warn "Can not refresh OAuth token since there is no refreshToken stored"
    } else {
        def refreshParams = [
            method: 'POST',
            uri   : apiEndpoint,
            path  : apiTokenPath,
            body : [grant_type: 'refresh_token', 
                    refresh_token: "${state.refreshToken}", 
                    client_id : smartThingsClientId,
                    client_secret: smartThingsClientSecret,
                    redirect_uri: callbackUrl],
        ]

        log.debug refreshParams

        def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the LaMetric (Connect) SmartApp and re-enter your account login credentials."
        //changed to httpPost
        try {
            def jsonMap
            httpPost(refreshParams) { resp ->
                if(resp.status == 200) {
                    log.debug "Token refreshed...calling saved RestAction now! $resp.data"
                    jsonMap = resp.data
                    if(resp.data) {
                        state.refreshToken = resp?.data?.refresh_token
                        state.authToken = resp?.data?.access_token
                        if(state.action && state.action != "") {
                            log.debug "Executing next action: ${state.action}"

                            "${state.action}"()

                            state.action = ""
                        }

                    } else {
                        log.warn ("No data in refresh token!");
                    }
                    state.action = ""
                }
            }
        } catch (groovyx.net.http.HttpResponseException e) {
            log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
            log.debug e.response.data;
            def reAttemptPeriod = 300 // in sec
            if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
                runIn(reAttemptPeriod, "refreshAuthToken")
            } else if (e.statusCode == 401) { // unauthorized
                state.reAttempt = state.reAttempt + 1
                log.warn "reAttempt refreshAuthToken to try = ${state.reAttempt}"
                if (state.reAttempt <= 3) {
                    runIn(reAttemptPeriod, "refreshAuthToken")
                } else {
                    sendPushAndFeeds(notificationMessage)
                    state.reAttempt = 0
                }
            }
        }
    }
}

def callback() {
    log.debug "callback()>> params: $params, params.code ${params.code}"

    def code = params.code
    def oauthState = params.state

    if (oauthState == state.oauthInitState){

        def tokenParams = [
            grant_type: "authorization_code",
            code      : code,
            client_id : smartThingsClientId,
            client_secret: smartThingsClientSecret,
            redirect_uri: callbackUrl
        ]
        log.trace tokenParams
        log.trace tokenUrl
        try {
            httpPost(uri: tokenUrl, body: tokenParams) { resp ->
                log.debug "swapped token: $resp.data"
                state.refreshToken = resp.data.refresh_token
                state.authToken = resp.data.access_token
            }
        } catch (e)
        {
            log.debug "fail ${e}";
        }
        if (state.authToken) {
            success()
        } else {
            fail()
        }
    } else {
        log.error "callback() failed oauthState != state.oauthInitState"
    }

}

def oauthInitUrl() {
    log.debug "oauthInitUrl with callback: ${callbackUrl}"

    state.oauthInitState = UUID.randomUUID().toString()

    def oauthParams = [
        response_type: "code",
        scope: composeScope(authScope),
        client_id: smartThingsClientId,
        state: state.oauthInitState,
        redirect_uri: callbackUrl
    ]
    log.debug oauthParams
    log.debug "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}"

    redirect(location: "${apiEndpoint}/api/v2/oauth2/authorize?${toQueryString(oauthParams)}")
}

def success() {
    def message = """
<p>Your LaMetric Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
    connectionStatus(message)
}

def fail() {
    def message = """
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
    connectionStatus(message)
}

def connectionStatus(message, redirectUrl = null) {
    def redirectHtml = ""
    if (redirectUrl) {
        redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
    }

    def html = """
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta content="width=device-width" id="viewport" name="viewport">
<style>
@font-face {
font-family: 'latoRegular';
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot");
src: url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.eot?#iefix") format("embedded-opentype"),
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.woff") format("woff"),
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.ttf") format("truetype"),
url("https://developer.lametric.com/assets/fonts/lato-regular-webfont.svg#latoRegular") format("svg");
font-style: normal;
font-weight: normal; }
.clearfix:after, .mobile .connect:after {
content: "";
clear: both;
display: table; }

.transition {
transition: all .3s ease 0s; }
html, body {
height: 100%;
}
body{
margin: 0;
padding: 0;
background: #f0f0f0;
color: #5c5c5c;
min-width: 1149px;
font-family: 'latoRegular', 'Lato';
}
.fixed-page #page {
min-height: 100%;
background: url(https://developer.lametric.com/assets/smart_things/page-bg.png) 50% 0 repeat-y;
}
.mobile {
min-width: 100%;
color: #757575; }
.mobile .wrap {
margin: 0 auto;
padding: 0;
max-width: 640px;
min-width: inherit; }
.mobile .connect {
width: 100%;
padding-top: 230px;
margin-bottom: 50px;
text-align: center; }
.mobile .connect img {
max-width: 100%;
height: auto;
vertical-align: middle;
display: inline-block;
margin-left: 2%;
border-radius: 15px; }
.mobile .connect img:first-child {
margin-left: 0; }
.mobile .info {
width: 100%;
margin: 0 auto;
margin-top: 50px;
margin-bottom: 50px; }
.mobile .info p {
max-width: 80%;
margin: 0 auto;
margin-top: 50px;
font-size: 28px;
line-height: 50px;
text-align: center; }

@media screen and (max-width: 639px) {
.mobile .connect{
padding-top: 100px; }
.mobile .wrap {
margin: 0 20px; }
.mobile .connect img {
width: 16%; }
.mobile .connect img:first-child, .mobile .connect img:last-child {
width: 40%; }
.mobile .info p{
font-size: 18px;
line-height: 24px;
margin-top: 20px; }
}
</style>
</head>
<body class="fixed-page mobile">

<div id="page">
<div class="wrap">

<div class="connect">
<img src="https://developer.lametric.com/assets/smart_things/product.png" width="190" height="190"><img src="https://developer.lametric.com/assets/smart_things/connected.png" width="87" height="19"><img src="https://developer.lametric.com/assets/smart_things/product-1.png" width="192" height="192">
</div>

<div class="info">
${message}
</div>
</div>
</div>
</body></html>
"""
    render contentType: 'text/html', data: html
}



//******************************************************************************************************************
//		 					 					 			LOCAL API
//******************************************************************************************************************

def getLocalApiDeviceInfoPath() 		{ "/api/v2/info" }
def getLocalApiSendNotificationPath()	{ "/api/v2/device/notifications" }
def getLocalApiIndexPath()				{ "/api/v2/device" }
def getLocalApiUser()					{ "dev" }


void requestDeviceInfo(localIp, apiKey)
{
    if (localIp && apiKey)
    {
        log.debug("request info ${localIp}");
        def command = new hubitat.device.HubAction([
            method: "GET",
            path: localApiDeviceInfoPath,
            headers: [
                HOST: "${localIp}:8080",
                Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
            ]])
        log.debug command
        sendHubCommand(command)
        command;
    } else {
        log.debug ("Unknown api key or ip address ${localIp} ${apiKey}")
    }
}

def sendNotificationMessageToDevice(dni, data)
{
    log.debug "send something"
    def device = resolveDNI2Device(dni);
    def localIp = device?.ipv4_internal;
    def apiKey = device?.api_key;
    if (localIp && apiKey)
    {
        log.debug "send notification message to device ${localIp}:8080 ${data}"
        sendHubCommand(new hubitat.device.HubAction([
            method: "POST",
            path: localApiSendNotificationPath,
            body: data,
            headers: [
                HOST: "${localIp}:8080",
                Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}",
                "Content-type":"application/json",
                "Accept":"application/json"
            ]]))
    }
}

def getAllInfoFromDevice(localIp, apiKey)
{
    log.debug "send something"
    if (localIp && apiKey)
    {
    	def hubCommand = new hubitat.device.HubAction([
            method: "GET",
            path: localApiIndexPath+"?fields=info,wifi,volume,bluetooth,id,name,mode,model,serial_number,os_version",
            headers: [
                HOST: "${localIp}:8080",
                Authorization: "Basic ${"${localApiUser}:${apiKey}".bytes.encodeBase64()}"
            ]])
        log.debug "sending request ${hubCommand}"
        sendHubCommand(hubCommand)
    }
}
//******************************************************************************************************************
//		 					 					 DEVICE HANDLER COMMANDs API
//******************************************************************************************************************

def resolveDNI2Device(dni)
{
    getDevices().find { it?.value?.dni == dni }?.value;
}

def requestRefreshDeviceInfo (dni)
{
    log.debug "device ${dni} request refresh";
    //	def devices = getDevices();
    //    def concreteDevice = devices[dni];
    //    requestDeviceInfo(conreteDevice);
}

private poll(dni) {
    def device = resolveDNI2Device(dni);
    def localIp = device?.ipv4_internal;
    def apiKey = device?.api_key;
    getAllInfoFromDevice(localIp, apiKey);
}

//******************************************************************************************************************
//		 					 					 			CLOUD METHODS
//******************************************************************************************************************


void listOfUserRemoteDevices()
{
    log.debug "get user device list"
    def deviceList = []
    if (state.accessToken)
    {
        def deviceListParams = [
            uri: apiEndpoint,
            path: apiUserMeDevicesList,
            headers: ["Content-Type": "text/json", "Authorization": "Bearer ${state.authToken}"]
        ]
        log.debug "making request ${deviceListParams}"
        def result;
        try {
            httpGet(deviceListParams){ resp ->
                if (resp.status == 200)
                {
                    deviceList = resp.data

                    def remoteDevices = getDevices();
                    for (deviceInfo in deviceList) {
                        if (deviceInfo)
                        {
                            def device = remoteDevices[deviceInfo.id.toString()]?:[:];
                                log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id.toString()]}";
                            for (it in deviceInfo ) {
                                device."${it.key}" = it.value;
                            }
                            remoteDevices."${deviceInfo.id}" = device;
                        } else {
                            log.debug ("empty device info")
                        }
                    }
                    verifyDevices();
                } else {
                    log.debug "http status: ${resp.status}"
                }
            }
        } catch (groovyx.net.http.HttpResponseException e)
        {
            log.debug("failed to get device list ${e}")
            def status = e.response.status
            if (status == 401) {
                state.action = "refreshDevices"
                log.debug "Refreshing your auth_token!"
                refreshAuthToken()
            }
            return;
        }
    } else {
        log.debug ("no access token to fetch user device list");
        return;
    }
}

Notification application

/**
 *  Lametric Notifier
 *
 *  Copyright 2016 Smart Atoms Ltd.
 *  Author: Mykola Kirichuk
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
 import groovy.json.JsonOutput
 
definition(
    name: "LaMetric Notifier",
    namespace: "com.lametric",
    author: "Mykola Kirichuk",
    description: "Allows you to send notifications to your LaMetric Time when something happens in your home to notify the whole family.",
    category: "Family",
    iconUrl: "https://developer.lametric.com/assets/smart_things/weather_60.png",
    iconX2Url: "https://developer.lametric.com/assets/smart_things/weather_120.png",
    iconX3Url: "https://developer.lametric.com/assets/smart_things/weather_120.png")


preferences {
	page(name: "mainPage", title: "Show a message on your LaMetric when something happens", install: true, uninstall: true)
	page(name: "timeIntervalInput", title: "Only during a certain time") {
		section {
			input "starting", "time", title: "Starting", required: false
			input "ending", "time", title: "Ending", required: false
		}
	}
}



           
def getSoundList() {
	[	
    	"none":"No Sound",
        "car" : "Car",
        "cash" : "Cash Register",
        "cat" : "Cat Meow",
        "dog" : "Dog Bark",
        "dog2" : "Dog Bark 2",
        "letter_email" : "The mail has arrived",
        "knock-knock" : "Knocking Sound",
        "bicycle" : "Bicycle",
        "negative1" : "Negative 1",
        "negative2" : "Negative 2",
        "negative3" : "Negative 3",
        "negative4" : "Negative 4",
        "negative5" : "Negative 5",
        "lose1" : "Lose 1",
        "lose2" : "Lose 2",
        "energy" : "Energy",
        "water1" : "Water 1",
        "water2" : "Water 2",
        "notification" : "Notification 1",
        "notification2" : "Notification 2",
        "notification3" : "Notification 3",
        "notification4" : "Notification 4",
        "open_door" : "Door unlocked",
        "win" : "Win",
        "win2" : "Win 2", 
        "positive1" : "Positive 1",
        "positive2" : "Positive 2",
        "positive3" : "Positive 3",
        "positive4" : "Positive 4",
        "positive5" : "Positive 5",
        "positive6" : "Positive 6",
        "statistic" : "Page turning",
        "wind" : "Wind",
        "wind_short" : "Small Wind",
    ] 
}

def getControlToAttributeMap(){
	[
        "motion": "motion.active",
        "contact": "contact.open",
        "contactClosed": "contact.close",
        "acceleration": "acceleration.active",
        "mySwitch": "switch.on",
        "mySwitchOff": "switch.off",
        "arrivalPresence": "presence.present",
        "departurePresence": "presence.not present",
        "smoke": "smoke.detected",
        "smoke1": "smoke.tested",
        "water": "water.wet",
        "button1": "button.pushed",
        "triggerModes": "mode",
        "timeOfDay": "time",
	]
}
                
def getPriorityList(){
	[
    	"warning":"Not So Important (may be ignored at night)",
        "critical": "Very Important"
    ]
}

def getIconsList(){
	state.icons = state.icons?:["1":"default"]
}


def getIconLabels() {
	state.iconLabels = state.iconLabels?:["1":"Default Icon"]
}

def getSortedIconLabels() {
	state.iconLabels = state.iconLabels?:["1":"Default Icon"]
    state.iconLabels.sort {a,b -> a.key.toInteger() <=> b.key.toInteger()};
}
def getLametricHost() { "https://developer.lametric.com" }
def getDefaultIconData() { """data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAe0lEQVQYlWNUVFBgYGBgYNi6bdt/BiTg7eXFyMDAwMCELBmz7z9DzL7/KBoYr127BpeEgbV64QzfRVYxMDAwMLAgSy5xYoSoeMPAwPkmjOG7yCqIgjf8WVC90xnQAdwKj7OZcMGD8m/hVjDBXLvDGKEbJunt5cXISMibAF0FMibYF7nMAAAAAElFTkSuQmCC""" }

def mainPage() {
    // WORKAROUND!!! To avoid state storage limitation we limit the number of icons API is returning to 1000.
    // TODO: Think about implementing correct pagination
    def iconRequestOptions = [headers: ["Accept": "application/json"],
						    uri: "${lametricHost}/api/v2/icons", query:["fields":"id,title,type,code", "order":"title", "page":"0", "page_size":"1000"]]

    	def icons = getIconsList();
        def iconLabels = getIconLabels();
        if (icons?.size() <= 2)
        {
	        log.debug iconRequestOptions
            try {
				httpGet(iconRequestOptions) { resp ->
                   	int i = 2;
                    resp.data.data.each(){
                        def iconId = it?.id
                        def iconType = it?.type
                        def prefix = "i"
                        if (iconId)
                        {
                            if (iconType == "movie")
                            {
                                prefix = "a"
                            }
                            def iconurl = "${lametricHost}/content/apps/icon_thumbs/${prefix}${iconId}_icon_thumb_big.png";
                            icons["$i"] = it.code
                            iconLabels["$i"] = it.title
                        } else {
                        	log.debug "wrong id"
                        }
                        ++i;
                    }
                }
            } catch (e)
            {
                log.debug "fail ${e}";
            }
        }
	dynamicPage(name: "mainPage") {
		def anythingSet = anythingSet()
        def notificationMessage = defaultNotificationMessage();
        log.debug "set $anythingSet"
		if (anythingSet) {
			section("Show message when"){
				ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true , submitOnChange:true
				ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
				ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
				ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
				ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
				ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
				ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
				ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
				ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
				ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange:true
				ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
				ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true, submitOnChange:true
				ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
			}
		}
		def hideable = anythingSet || app.installationState == "COMPLETE"
		def sectionTitle = anythingSet ? "Select additional triggers" : "Show message when..."

		section(sectionTitle, hideable: hideable, hidden: true){
			ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true, submitOnChange:true
			ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true, submitOnChange:true
			ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true, submitOnChange:true
			ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true, submitOnChange:true
			ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true, submitOnChange:true
			ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true, submitOnChange:true
			ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true, submitOnChange:true
			ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true, submitOnChange:true
			ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true, submitOnChange:true
			ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true, submitOnChange:true
			ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
			ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true, submitOnChange:true
			ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false, submitOnChange:true
		}

		section (title:"Select LaMetrics"){
			input "selectedDevices", "capability.notification", required: true, multiple:true
		}
        section (title: "Configure message"){
        	input "defaultMessage", "bool", title: "Use Default Text:\n\"$notificationMessage\"", required: false, defaultValue: true, submitOnChange:true
			input "showCustomIcon", "bool", title:"Use custom icon", reqired: false, defaultValue:true, submitOnChange:true
	        def showMessageInput = (settings["defaultMessage"] == null || settings["defaultMessage"] == true) ? false : true;
			if (showMessageInput)
        	{
             	input "customMessage","text",title:"Use Custom Text", defaultValue:"", required:true, multiple: false
        	}
   	        def showCustomIcon = (settings["showCustomIcon"] == null || settings["showCustomIcon"] == true) ? true : false;
            if (showCustomIcon)
            {
	            input "customIcon","number",title:"Use Icon Id", defaultValue:"", required:true, multiple: false
            } else {
        		input "selectedIcon", "enum", title: "With Icon", required: false, multiple: false, defaultValue:"1", options: getSortedIconLabels()
            }
   			input "selectedSound", "enum", title: "With Sound", required: true, defaultValue:"none" , options: soundList
			input "showPriority", "enum", title: "Is This Notification Very Important?", required: true, multiple:false, defaultValue: "warning", options: priorityList
		}
		section("More options", hideable: true, hidden: true) {
			href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
			input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
				options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
			if (settings.modes) {
            	input "modes", "mode", title: "Only when mode is", multiple: true, required: false
            }
		}
		section([mobileOnly:true]) {
			label title: "Assign a name", required: false
			mode title: "Set for specific mode(s)", required: false
		}
	}
}

private songOptions() {
		log.trace "song option"
	// Make sure current selection is in the set

	def options = new LinkedHashSet()
	if (state.selectedSong?.station) {
		options << state.selectedSong.station
	}
	else if (state.selectedSong?.description) {
		// TODO - Remove eventually? 'description' for backward compatibility
		options << state.selectedSong.description
	}

	// Query for recent tracks
	def states = sonos.statesSince("trackData", new Date(0), [max:30])
	def dataMaps = states.collect{it.jsonValue}
	options.addAll(dataMaps.collect{it.station})

	log.trace "${options.size()} songs in list"
	options.take(20) as List
}

private anythingSet() {
	for (it in controlToAttributeMap) {
    	log.debug ("key ${it.key} value ${settings[it.key]} ${settings[it.key]?true:false}")
		if (settings[it.key]) {
	        log.debug constructMessageFor(it.value, settings[it.key])
			return true
		}
	}
	return false
}

def defaultNotificationMessage(){
	def message = "";
	for (it in controlToAttributeMap)  {
		if (settings[it.key]) {
	        message = constructMessageFor(it.value, settings[it.key])
            break;
		}
	}
	return message;
}

def constructMessageFor(group, device)
{
	log.debug ("$group $device")
	def message = "";
    def firstDevice;
    if (device instanceof List)
    {
    	firstDevice = device[0];
    } else {
    	firstDevice = device;
    }
    switch(group)
    {
    	case "motion.active":
        	message = "Motion detected by $firstDevice.displayName at $location.name"
        break;
        case "contact.open":
        	message = "Openning detected by $firstDevice.displayName at $location.name"
        break;
		case "contact.closed":
        	message = "Closing detected by $firstDevice.displayName at $location.name"
        break;
        case "acceleration.active":
        	message = "Acceleration detected by $firstDevice.displayName at $location.name"
        break;
        case "switch.on":
        	message = "$firstDevice.displayName turned on at $location.name"
        break;
        case "switch.off":
        	message = "$firstDevice.displayName turned off at $location.name"
        break;
        case "presence.present":
	        message = "$firstDevice.displayName detected arrival at $location.name"
        break;
        case "presence.not present":
	        message = "$firstDevice.displayName detected departure at $location.name"
        break;
        case "smoke.detected":
        	message = "Smoke detected by $firstDevice.displayName at $location.name"
        break;
         case "smoke.tested":
        	message = "Smoke tested by $firstDevice.displayName at $location.name"
        break;
        case "water.wet":
        	message = "Dampness detected by $firstDevice.displayName at $location.name"
        break;
        case "button.pushed":
	        message = "$firstDevice.displayName pushed at $location.name"
		break;
        case "time":
        case "time.":
            message = "Scheduled notification"
        break;
        case "mode":
        	message = "Mode changed at $location.name"
        break;
    }
    
    for (mode in location.modes) {
        if ("mode.$mode" == group) {
            message = "Mode changed to $location.currentMode at $location.name";
            break;
        }
    }
    return message;
}

private ifUnset(Map options, String name, String capability) {
	if (!settings[name]) {
		input(options, name, capability)
	}
}

private ifSet(Map options, String name, String capability) {
	if (settings[name]) {
		input(options, name, capability)
	}
}

def installed() {

	log.debug "Installed with settings: ${settings}"
	subscribeToEvents()
}

def updated() {
	log.debug "Updated with settings: ${settings}"
	unsubscribe()
	unschedule()
	subscribeToEvents()
}

def subscribeToEvents() {
	log.trace "subscribe to events"
    log.debug "${contact} ${contactClosed} ${mySwitch} ${mySwitchOff} ${acceleration}${arrivalPresence} ${button1}"
//	subscribe(app, appTouchHandler)
	subscribe(contact, "contact.open", eventHandler)
	subscribe(contactClosed, "contact.closed", eventHandler)
	subscribe(acceleration, "acceleration.active", eventHandler)
	subscribe(motion, "motion.active", eventHandler)
	subscribe(mySwitch, "switch.on", eventHandler)
	subscribe(mySwitchOff, "switch.off", eventHandler)
	subscribe(arrivalPresence, "presence.present", eventHandler)
	subscribe(departurePresence, "presence.not present", eventHandler)
	subscribe(smoke, "smoke.detected", eventHandler)
	subscribe(smoke, "smoke.tested", eventHandler)
	subscribe(smoke, "carbonMonoxide.detected", eventHandler)
	subscribe(water, "water.wet", eventHandler)
	subscribe(button1, "button.pushed", eventHandler)

	if (triggerModes) {
		subscribe(location, modeChangeHandler)
	}

	if (timeOfDay) {
		schedule(timeOfDay, scheduledTimeHandler)
	}
}

def eventHandler(evt) {
	log.trace "eventHandler(${evt?.name}: ${evt?.value})"
    def name = evt?.name;
    def value = evt?.value;
    
	if (allOk) {
			log.trace "allOk"
 			takeAction(evt)
		}
		else {
			log.debug "Not taking action because it was already taken today"
		}
}
def modeChangeHandler(evt) {
	log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
	if (evt?.value in triggerModes) {
		eventHandler(evt)
	}
}

def scheduledTimeHandler() {
    def evt = [name:"time", value:"", device : ""];
	eventHandler(evt)
}

def appTouchHandler(evt) {
	takeAction(evt)
}

private takeAction(evt) {
    if(evt == null) {
        log.debug "NPE in takeAction"
        return;
    }
	def messageToShow
    if (defaultMessage)
    {
    	messageToShow = constructMessageFor("${evt.name}.${evt.value}", evt.device);
    } else {
    	messageToShow = customMessage;
    }
	if (messageToShow)
    {
    	log.debug "text ${messageToShow}"
    	def notification = [:];
        def frame1 = [:];
        frame1.text = messageToShow;
        if (customIcon && showCustomIcon)
        {
        	frame1.icon = customIcon;
        } else {
            if (selectedIcon != "1")
            {
                frame1.icon = state.icons[selectedIcon];
            } else {
                frame1.icon = defaultIconData;
            }
        }
        def soundId = sound;
        def sound = [:];
        sound.id = selectedSound;
        sound.category = "notifications";
        def frames = [];
        frames << frame1;
        def model = [:];
        model.frames = frames;
        if (selectedSound != "none")
        {
        	model.sound = sound;
        }
        notification.model = model;
        notification.priority = showPriority;
        def serializedData = new JsonOutput().toJson(notification);

        selectedDevices.each { lametricDevice ->
            log.trace "send notification to ${lametricDevice} ${serializedData}"
        	lametricDevice.deviceNotification(serializedData)
        }
    } else {
    	log.debug "No message to show"
    }
	
	log.trace "Exiting takeAction()"
}

private frequencyKey(evt) {
	"lastActionTimeStamp"
}

private dayString(Date date) {
	def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
	if (location.timeZone) {
		df.setTimeZone(location.timeZone)
	}
	else {
		df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
	}
	df.format(date)
}

private oncePerDayOk(Long lastTime) {
	def result = true
	if (oncePerDay) {
		result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
		log.trace "oncePerDayOk = $result"
	}
	result
}

// TODO - centralize somehow
private getAllOk() {
	modeOk && daysOk && timeOk
}

private getModeOk() {
	def result = !modes || modes.contains(location.mode)
	log.trace "modeOk = $result"
	result
}

private getDaysOk() {
	def result = true
	if (days) {
		def df = new java.text.SimpleDateFormat("EEEE")
		if (location.timeZone) {
			df.setTimeZone(location.timeZone)
		}
		else {
			df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
		}
		def day = df.format(new Date())
		result = days.contains(day)
	}
	log.trace "daysOk = $result"
	result
}

private getTimeOk() {
	def result = true
	if (starting && ending) {
		def currTime = now()
		def start = timeToday(starting, location?.timeZone).time
		def stop = timeToday(ending, location?.timeZone).time
		result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
	}
	log.trace "timeOk = $result"
	result
}

private hhmm(time, fmt = "h:mm a")
{
	def t = timeToday(time, location.timeZone)
	def f = new java.text.SimpleDateFormat(fmt)
	f.setTimeZone(location.timeZone ?: timeZone(time))
	f.format(t)
}

private getTimeLabel()
{
	(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}

Now keep in mind I do not have Lametric. I just imported the code and changed settings that were causing errors. You will need to enable OAUTH in the two apps based on the code. Hope this gets people started.

Thanks, I will try it later today. This was their response to me:

"Thanks for contacting us.

Currently we do not have immediate plans regarding this integration as we keep working on other awesome features now. But for sure we'll keep this in mind."

@aaron, thanks for the attempt. It won't get past the OAuth request.

Did you enable oauth?

Hey gang.

Has any progress been made past this point?

I DO have a LaMetric, so I've been having a bit of play around. I've got OAUTH sufficiently enabled, got existing LaMetric developer accounts, etc...

It makes it as far as 'Click to Enter LaMetric Credentials', and then we die on the click with an OAUTH bit of XML:

<oauth>

<error_description>24b702b3-b325-487d-9b62-4a7efb16d454</error_description>

<error>invalid_token</error>

</oauth>

I'll try to look into it in more detail, but it won't be today, I'm afraid.

Best Regards,

Jules

Ah! OK. I see the next problem. It's still trying to do oauth via graph.api.smartthings.com - that's probably not going to work :wink:

Also... looks like hubitat doesn't understand appSettings - so I'll have to hard-code the Client ID and Secret.

-- Jules

I never got it connected to HE, but it wasn't a priority. If you can make it work, that would be great.

Hehe. I'll give it a go.

Might have jumped in more at the deep-end than I'd originally thought - but hey... you learn best by doing, and all that.

My mental map of how OAuth works is clearly deficient at the moment, but I suspect that if I can work it out, then it'll probably admit defeat eventually. I'll let you know how I get on. LaMetric is hardly the highest priority for me, either... but it looked like low-hanging fruit when I sat down and tried to work out what I needed to do :wink:

-- Jules

1 Like

Interested to see this. There's someone that can do a lot for the optics of this platform if he has reason to want to try it. He has a LaMetric and this would be a good carrot to dangle.

Oh, by the way, if you contact @bobbyD, he can change your user name to one that we humans can read :wink:

Oh yeah. I'd only just noticed that I've got a name only CPUs can pronounce :wink:

@bobbyD - any chance of a name change? Either 'MrTickle', or 'JulesT' would do nicely.

-- Jules

1 Like

OK. Finally. Success, I think.

So... quite a lot of changes necessary to get it working in the end, I think, but all confined to the Connect app.

In no particular order, changes to the following:

  1. Added web interface to make ClientID and ClientSecret actually turn up and be prompted for.
  2. Re-wrote the Oauth stuff pretty much from scratch (Well... I say from Scratch - I mostly cribbed it from other integrations I found on the forums here).
  3. Not sure I'm doing this right - but the way the discovery worked was to do a sendHubCommand to interrogate a discovered device, and assume that the locationHandler would get a callback when the response arrived. That wasn't happening, so I replaced it with an explicit callback, which then handles the data and correctly populates it's device data.
  4. Notifications - they were using sendHubCommand too, but from reading the forums, they don't work in drivers. I took that to mean that they don't work if they're LAUNCHED from drivers - technically the code was in the Connect app, and called by a parent(). in the driver. I replaced that with an asynchttpPost() call with an appropriate callback, and all looks to be well now.

So... anybody that's interested, please feel free to give it a go. The code can be found here:

Connect App: https://raw.githubusercontent.com/Slapn/hubitat-LaMetric/master/apps/lametric-connect.groovy
Notifier App (largely unchanged): https://raw.githubusercontent.com/Slapn/hubitat-LaMetric/master/apps/lametric-notifier.groovy
Driver: https://raw.githubusercontent.com/Slapn/hubitat-LaMetric/master/device/lametric-device.groovy

Please let me know how you get on. I'm not ruling out that it explodes violently for all involved :wink:

-- Jules

3 Likes

Hello, I don't have a lametric at the moment but searching for a way to display meassured temperatures and humidity aso at a glance.

How capable is this implementation?

I never got it to work with HE. I still use the Lametric, but on it's own. It also displays what is playing on my Sonos.

so it's not really useful for an integration... thanks