Compare commits

..

8 Commits

Author SHA1 Message Date
Brian Richards
6e3683c5ad MSA-1357: Nest Temp Info 2016-06-16 12:45:04 -05:00
Vinay Rao
eac48382e8 Merge pull request #993 from SmartThingsCommunity/staging
Rolling down hotfix to master
2016-06-14 15:55:53 -07:00
Vinay Rao
9f09a4b0b2 Merge pull request #991 from SmartThingsCommunity/production
Rolling down hotfix
2016-06-14 13:10:03 -07:00
Donald C. Kirker
3c47fe7b60 Merge pull request #990 from dkirker/astra-devices
Add re-type for Vision Motion Sensor Plus.
2016-06-14 11:41:04 -07:00
Donald Kirker
45a0822e9b Add re-type for Vision Motion Sensor Plus. 2016-06-14 11:07:31 -07:00
Vinay Rao
a94a62d34c Merge pull request #982 from rohandesai/netatmo-hotfix
NPE fix for NetAtmo
2016-06-09 11:30:51 -07:00
Rohan Desai
e861d3c256 added fix for NPE 2016-06-09 11:04:55 -07:00
Vinay Rao
8d8b039dda Merge pull request #975 from SmartThingsCommunity/staging
Rolling up staging to production
2016-06-07 12:23:55 -07:00
6 changed files with 611 additions and 1507 deletions

View File

@@ -1,126 +0,0 @@
/**
* 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
}
main (["rich-control"])
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
}*/

View File

@@ -0,0 +1,600 @@
/**
* Nest Direct
*
* Author: dianoga7@3dgo.net
* Code: https://github.com/smartthings-users/device-type.nest
*
* INSTALLATION
* =========================================
* 1) Create a new device type (https://graph.api.smartthings.com/ide/devices)
* Name: Nest
* Author: dianoga7@3dgo.net
* Capabilities:
* Polling
* Relative Humidity Measurement
* Thermostat
* Temperature Measurement
* Presence Sensor
* Sensor
* Custom Attributes:
* temperatureUnit
* Custom Commands:
* away
* present
* setPresence
* heatingSetpointUp
* heatingSetpointDown
* coolingSetpointUp
* coolingSetpointDown
* setFahrenheit
* setCelsius
*
* 2) If you want to switch from slider controls to buttons, comment out the slider details line and uncomment the button details line.
*
* 3) Create a new device (https://graph.api.smartthings.com/device/list)
* Name: Your Choice
* Device Network Id: Your Choice
* Type: Nest (should be the last option)
* Location: Choose the correct location
* Hub/Group: Leave blank
*
* 4) Update device preferences
* Click on the new device to see the details.
* Click the edit button next to Preferences
* Fill in your information.
* To find your serial number, login to http://home.nest.com. Click on the thermostat
* you want to control. Under settings, go to Technical Info. Your serial number is
* the second item.
*
* 5) That's it, you're done.
*
* Copyright (C) 2013 Brian Steere <dianoga7@3dgo.net>
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions: The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
preferences {
input("username", "text", title: "Username", description: "Your Nest username (usually an email address)")
input("password", "password", title: "Password", description: "Your Nest password")
input("serial", "text", title: "Serial #", description: "The serial number of your thermostat")
}
// for the UI
metadata {
definition (name: "Nest", namespace: "smartthings-users", author: "dianoga7@3dgo.net") {
capability "Polling"
capability "Relative Humidity Measurement"
capability "Thermostat"
capability "Temperature Measurement"
capability "Presence Sensor"
capability "Sensor"
command "away"
command "present"
command "setPresence"
command "heatingSetpointUp"
command "heatingSetpointDown"
command "coolingSetpointUp"
command "coolingSetpointDown"
command "setFahrenheit"
command "setCelsius"
attribute "temperatureUnit", "string"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
valueTile("temperature", "device.temperature", canChangeIcon: true, canChangeBackground:true) {
state("temperature", label: '${currentValue}°', backgroundColors: [
// Celsius Color Range
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 29, color: "#f1d801"],
[value: 33, color: "#d04e00"],
[value: 36, color: "#bc2323"],
// Fahrenheit Color Range
[value: 40, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 92, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: true, decoration: "flat") {
state("auto", action:"thermostat.off", icon: "st.thermostat.auto")
state("off", action:"thermostat.cool", icon: "st.thermostat.heating-cooling-off")
state("cool", action:"thermostat.heat", icon: "st.thermostat.cool")
state("heat", action:"thermostat.auto", icon: "st.thermostat.heat")
}
standardTile("thermostatFanMode", "device.thermostatFanMode", inactiveLabel: true, decoration: "flat") {
state "auto", action:"thermostat.fanOn", icon: "st.thermostat.fan-auto"
state "on", action:"thermostat.fanCirculate", icon: "st.thermostat.fan-on"
state "circulate", action:"thermostat.fanAuto", icon: "st.thermostat.fan-circulate"
}
valueTile("heatingSetpoint", "device.heatingSetpoint") {
state "default", label:'${currentValue}°', unit:"Heat", backgroundColor:"#bc2323"
}
valueTile("coolingSetpoint", "device.coolingSetpoint") {
state "default", label:'${currentValue}°', unit:"Cool", backgroundColor:"#1e9cbb"
}
standardTile("thermostatOperatingState", "device.thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
state "idle", action:"polling.poll", label:'${name}', icon: "st.sonos.pause-icon"
state "cooling", action:"polling.poll", label:' ', icon: "st.thermostat.cooling", backgroundColor:"#1e9cbb"
state "heating", action:"polling.poll", label:' ', icon: "st.thermostat.heating", backgroundColor:"#bc2323"
state "fan only", action:"polling.poll", label:'${name}', icon: "st.Appliances.appliances11"
}
valueTile("humidity", "device.humidity", inactiveLabel: false) {
state "default", label:'${currentValue}%', unit:"Humidity"
}
standardTile("presence", "device.presence", inactiveLabel: false, decoration: "flat") {
state "present", label:'${name}', action:"away", icon: "st.Home.home2"
state "not present", label:'away', action:"present", icon: "st.Transportation.transportation5"
}
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
state "default", action:"polling.poll", icon:"st.secondary.refresh"
}
standardTile("temperatureUnit", "device.temperatureUnit", canChangeIcon: false) {
state "fahrenheit", label: "°F", icon: "st.alarm.temperature.normal", action:"setCelsius"
state "celsius", label: "°C", icon: "st.alarm.temperature.normal", action:"setFahrenheit"
}
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
state "setHeatingSetpoint", label:'Set temperature to', action:"thermostat.setHeatingSetpoint"
}
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
state "setCoolingSetpoint", label:'Set temperature to', action:"thermostat.setCoolingSetpoint"
}
standardTile("heatingSetpointUp", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
state "heatingSetpointUp", label:' ', action:"heatingSetpointUp", icon:"st.thermostat.thermostat-up", backgroundColor:"#bc2323"
}
standardTile("heatingSetpointDown", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
state "heatingSetpointDown", label:' ', action:"heatingSetpointDown", icon:"st.thermostat.thermostat-down", backgroundColor:"#bc2323"
}
standardTile("coolingSetpointUp", "device.coolingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
state "coolingSetpointUp", label:' ', action:"coolingSetpointUp", icon:"st.thermostat.thermostat-up", backgroundColor:"#1e9cbb"
}
standardTile("coolingSetpointDown", "device.coolingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
state "coolingSetpointDown", label:' ', action:"coolingSetpointDown", icon:"st.thermostat.thermostat-down", backgroundColor:"#1e9cbb"
}
main(["temperature", "thermostatOperatingState", "humidity"])
// ============================================================
// Slider or Buttons...
// To expose buttons, comment out the first detials line below and uncomment the second details line below.
// To expose sliders, uncomment the first details line below and comment out the second details line below.
details(["temperature", "thermostatOperatingState", "humidity", "thermostatMode", "thermostatFanMode", "presence", "heatingSetpoint", "heatSliderControl", "coolingSetpoint", "coolSliderControl", "temperatureUnit", "refresh"])
// details(["temperature", "thermostatOperatingState", "humidity", "thermostatMode", "thermostatFanMode", "presence", "heatingSetpointDown", "heatingSetpoint", "heatingSetpointUp", "coolingSetpointDown", "coolingSetpoint", "coolingSetpointUp", "temperatureUnit", "refresh"])
// ============================================================
}
}
// parse events into attributes
def parse(String description) {
}
// handle commands
def setHeatingSetpoint(temp) {
def latestThermostatMode = device.latestState('thermostatMode')
def temperatureUnit = device.latestValue('temperatureUnit')
switch (temperatureUnit) {
case "celsius":
if (temp) {
if (temp < 9) {
temp = 9
}
if (temp > 32) {
temp = 32
}
if (latestThermostatMode.stringValue == 'auto') {
api('temperature', ['target_change_pending': true, 'target_temperature_low': temp]) {
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
}
} else if (latestThermostatMode.stringValue == 'heat') {
api('temperature', ['target_change_pending': true, 'target_temperature': temp]) {
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
}
}
}
break;
default:
if (temp) {
if (temp < 51) {
temp = 51
}
if (temp > 89) {
temp = 89
}
if (latestThermostatMode.stringValue == 'auto') {
api('temperature', ['target_change_pending': true, 'target_temperature_low': fToC(temp)]) {
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
}
} else if (latestThermostatMode.stringValue == 'heat') {
api('temperature', ['target_change_pending': true, 'target_temperature': fToC(temp)]) {
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
}
}
}
break;
}
poll()
}
def coolingSetpointUp(){
int newSetpoint = device.currentValue("coolingSetpoint") + 1
log.debug "Setting cool set point up to: ${newSetpoint}"
setCoolingSetpoint(newSetpoint)
}
def coolingSetpointDown(){
int newSetpoint = device.currentValue("coolingSetpoint") - 1
log.debug "Setting cool set point down to: ${newSetpoint}"
setCoolingSetpoint(newSetpoint)
}
def setCoolingSetpoint(temp) {
def latestThermostatMode = device.latestState('thermostatMode')
def temperatureUnit = device.latestValue('temperatureUnit')
switch (temperatureUnit) {
case "celsius":
if (temp) {
if (temp < 9) {
temp = 9
}
if (temp > 32) {
temp = 32
}
if (latestThermostatMode.stringValue == 'auto') {
api('temperature', ['target_change_pending': true, 'target_temperature_high': temp]) {
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
}
} else if (latestThermostatMode.stringValue == 'cool') {
api('temperature', ['target_change_pending': true, 'target_temperature': temp]) {
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
}
}
}
break;
default:
if (temp) {
if (temp < 51) {
temp = 51
}
if (temp > 89) {
temp = 89
}
if (latestThermostatMode.stringValue == 'auto') {
api('temperature', ['target_change_pending': true, 'target_temperature_high': fToC(temp)]) {
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
}
} else if (latestThermostatMode.stringValue == 'cool') {
api('temperature', ['target_change_pending': true, 'target_temperature': fToC(temp)]) {
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
}
}
}
break;
}
poll()
}
def heatingSetpointUp(){
int newSetpoint = device.currentValue("heatingSetpoint") + 1
log.debug "Setting heat set point up to: ${newSetpoint}"
setHeatingSetpoint(newSetpoint)
}
def heatingSetpointDown(){
int newSetpoint = device.currentValue("heatingSetpoint") - 1
log.debug "Setting heat set point down to: ${newSetpoint}"
setHeatingSetpoint(newSetpoint)
}
def setFahrenheit() {
def temperatureUnit = "fahrenheit"
log.debug "Setting temperatureUnit to: ${temperatureUnit}"
sendEvent(name: "temperatureUnit", value: temperatureUnit)
poll()
}
def setCelsius() {
def temperatureUnit = "celsius"
log.debug "Setting temperatureUnit to: ${temperatureUnit}"
sendEvent(name: "temperatureUnit", value: temperatureUnit)
poll()
}
def off() {
setThermostatMode('off')
}
def heat() {
setThermostatMode('heat')
}
def emergencyHeat() {
setThermostatMode('heat')
}
def cool() {
setThermostatMode('cool')
}
def auto() {
setThermostatMode('range')
}
def setThermostatMode(mode) {
mode = mode == 'emergency heat'? 'heat' : mode
api('thermostat_mode', ['target_change_pending': true, 'target_temperature_type': mode]) {
mode = mode == 'range' ? 'auto' : mode
sendEvent(name: 'thermostatMode', value: mode)
poll()
}
}
def fanOn() {
setThermostatFanMode('on')
}
def fanAuto() {
setThermostatFanMode('auto')
}
def fanCirculate() {
setThermostatFanMode('circulate')
}
def setThermostatFanMode(mode) {
def modes = [
on: ['fan_mode': 'on'],
auto: ['fan_mode': 'auto'],
circulate: ['fan_mode': 'duty-cycle', 'fan_duty_cycle': 900]
]
api('fan_mode', modes.getAt(mode)) {
sendEvent(name: 'thermostatFanMode', value: mode)
poll()
}
}
def away() {
setPresence('away')
sendEvent(name: 'presence', value: 'not present')
}
def present() {
setPresence('present')
sendEvent(name: 'presence', value: 'present')
}
def setPresence(status) {
log.debug "Status: $status"
api('presence', ['away': status == 'away', 'away_timestamp': new Date().getTime(), 'away_setter': 0]) {
poll()
}
}
def poll() {
log.debug "Executing 'poll'"
api('status', []) {
data.device = it.data.device.getAt(settings.serial)
data.shared = it.data.shared.getAt(settings.serial)
data.structureId = it.data.link.getAt(settings.serial).structure.tokenize('.')[1]
data.structure = it.data.structure.getAt(data.structureId)
data.device.fan_mode = data.device.fan_mode == 'duty-cycle'? 'circulate' : data.device.fan_mode
data.structure.away = data.structure.away ? 'away' : 'present'
log.debug(data.shared)
def humidity = data.device.current_humidity
def temperatureType = data.shared.target_temperature_type
def fanMode = data.device.fan_mode
def heatingSetpoint = '--'
def coolingSetpoint = '--'
temperatureType = temperatureType == 'range' ? 'auto' : temperatureType
sendEvent(name: 'humidity', value: humidity)
sendEvent(name: 'thermostatFanMode', value: fanMode)
sendEvent(name: 'thermostatMode', value: temperatureType)
def temperatureUnit = device.latestValue('temperatureUnit')
switch (temperatureUnit) {
case "celsius":
def temperature = Math.round(data.shared.current_temperature)
def targetTemperature = Math.round(data.shared.target_temperature)
if (temperatureType == "cool") {
coolingSetpoint = targetTemperature
} else if (temperatureType == "heat") {
heatingSetpoint = targetTemperature
} else if (temperatureType == "auto") {
coolingSetpoint = Math.round(data.shared.target_temperature_high)
heatingSetpoint = Math.round(data.shared.target_temperature_low)
}
sendEvent(name: 'temperature', value: temperature, unit: temperatureUnit, state: temperatureType)
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
break;
default:
def temperature = Math.round(cToF(data.shared.current_temperature))
def targetTemperature = Math.round(cToF(data.shared.target_temperature))
if (temperatureType == "cool") {
coolingSetpoint = targetTemperature
} else if (temperatureType == "heat") {
heatingSetpoint = targetTemperature
} else if (temperatureType == "auto") {
coolingSetpoint = Math.round(cToF(data.shared.target_temperature_high))
heatingSetpoint = Math.round(cToF(data.shared.target_temperature_low))
}
sendEvent(name: 'temperature', value: temperature, unit: temperatureUnit, state: temperatureType)
sendEvent(name: 'coolingSetpoint', value: coolingSetpoint, unit: temperatureUnit, state: "cool")
sendEvent(name: 'heatingSetpoint', value: heatingSetpoint, unit: temperatureUnit, state: "heat")
break;
}
switch (device.latestValue('presence')) {
case "present":
if (data.structure.away == 'away') {
sendEvent(name: 'presence', value: 'not present')
}
break;
case "not present":
if (data.structure.away == 'present') {
sendEvent(name: 'presence', value: 'present')
}
break;
}
if (data.shared.hvac_ac_state) {
sendEvent(name: 'thermostatOperatingState', value: "cooling")
} else if (data.shared.hvac_heater_state) {
sendEvent(name: 'thermostatOperatingState', value: "heating")
} else if (data.shared.hvac_fan_state) {
sendEvent(name: 'thermostatOperatingState', value: "fan only")
} else {
sendEvent(name: 'thermostatOperatingState', value: "idle")
}
}
}
def api(method, args = [], success = {}) {
if(!isLoggedIn()) {
log.debug "Need to login"
login(method, args, success)
return
}
def methods = [
'status': [uri: "/v2/mobile/${data.auth.user}", type: 'get'],
'fan_mode': [uri: "/v2/put/device.${settings.serial}", type: 'post'],
'thermostat_mode': [uri: "/v2/put/shared.${settings.serial}", type: 'post'],
'temperature': [uri: "/v2/put/shared.${settings.serial}", type: 'post'],
'presence': [uri: "/v2/put/structure.${data.structureId}", type: 'post']
]
def request = methods.getAt(method)
log.debug "Logged in"
doRequest(request.uri, args, request.type, success)
}
// Need to be logged in before this is called. So don't call this. Call api.
def doRequest(uri, args, type, success) {
log.debug "Calling $type : $uri : $args"
if(uri.charAt(0) == '/') {
uri = "${data.auth.urls.transport_url}${uri}"
}
def params = [
uri: uri,
headers: [
'X-nl-protocol-version': 1,
'X-nl-user-id': data.auth.userid,
'Authorization': "Basic ${data.auth.access_token}"
],
body: args
]
def postRequest = { response ->
if (response.getStatus() == 302) {
def locations = response.getHeaders("Location")
def location = locations[0].getValue()
log.debug "redirecting to ${location}"
doRequest(location, args, type, success)
} else {
success.call(response)
}
}
try {
if (type == 'post') {
httpPostJson(params, postRequest)
} else if (type == 'get') {
httpGet(params, postRequest)
}
} catch (Throwable e) {
login()
}
}
def login(method = null, args = [], success = {}) {
def params = [
uri: 'https://home.nest.com/user/login',
body: [username: settings.username, password: settings.password]
]
httpPost(params) {response ->
data.auth = response.data
data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime()
log.debug data.auth
api(method, args, success)
}
}
def isLoggedIn() {
if(!data.auth) {
log.debug "No data.auth"
return false
}
def now = new Date().getTime();
return data.auth.expires_in > now
}
def cToF(temp) {
return (temp * 1.8 + 32).toDouble()
}
def fToC(temp) {
return ((temp - 32) / 1.8).toDouble()
}

View File

@@ -29,6 +29,7 @@ metadata {
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x86,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98,0x86,0x72,0x5A,0x85,0x59,0x73,0x80,0x71,0x31,0x70,0x84,0x7A" // Vision Motion
}
// simulator metadata
@@ -263,5 +264,9 @@ def retypeBasedOnMSR() {
log.debug "Changing device type to Door / Window Sensor Plus (SG)"
setDeviceType("Door / Window Sensor Plus (SG)")
break
case "0109-2002-0205": // Vision Motion
log.debug "Changing device type to Vision Motion Sensor Plus (SG)"
setDeviceType("Vision Motion Sensor Plus (SG)")
break
}
}

View File

@@ -1,839 +0,0 @@
/**
* 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 physicalgraph.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 physicalgraph.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 physicalgraph.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}"?:[:];
log.debug "before list ${device} ${deviceInfo} ${deviceInfo.id} ${remoteDevices[deviceInfo.id]}";
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;
}
}

View File

@@ -1,536 +0,0 @@
/**
* 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() {
def iconRequestOptions = [headers: ["Accept": "application/json"],
uri: "${lametricHost}/api/v2/icons", query:["fields":"id,title,type,code", "order":"title"]]
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
def showMessageInput = (settings["defaultMessage"] == null || settings["defaultMessage"] == true) ? false : true;
if (showMessageInput)
{
input "customMessage","text",title:"Use Custom Text", defaultValue:"", required:false, multiple: false
}
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":
break;
// case "mode":
// message = "Mode changed to ??? 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() {
eventHandler(null)
}
def appTouchHandler(evt) {
takeAction(evt)
}
private takeAction(evt) {
log.trace "takeAction()"
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 (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") : ""
}

View File

@@ -337,10 +337,10 @@ def initialize() {
settings.devices.each {
def deviceId = it
def detail = state.deviceDetail[deviceId]
def detail = state?.deviceDetail[deviceId]
try {
switch(detail.type) {
switch(detail?.type) {
case 'NAMain':
log.debug "Base station"
createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
@@ -487,12 +487,12 @@ def poll() {
log.debug "State: ${state.deviceState}"
settings.devices.each { deviceId ->
def detail = state.deviceDetail[deviceId]
def data = state.deviceState[deviceId]
def child = children.find { it.deviceNetworkId == deviceId }
def detail = state?.deviceDetail[deviceId]
def data = state?.deviceState[deviceId]
def child = children?.find { it.deviceNetworkId == deviceId }
log.debug "Update: $child";
switch(detail.type) {
switch(detail?.type) {
case 'NAMain':
log.debug "Updating NAMain $data"
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())