Compare commits

..

15 Commits

Author SHA1 Message Date
Juan Pablo Risso
9b285ec93b PRP-151 - Harmony Cloud (#1003)
- Added isIP() to check if a local or cloud callback URL
- Added try around Integer.parseInt
2016-06-21 13:19:53 -04:00
Juan Pablo Risso
777f8f7e20 PENG-161 - Logitech Harmony don't allow undefined commands (#965)
https://smartthings.atlassian.net/browse/PENG-161

extra )

New getCapabilityName()

Small fixes

Extra colon

capName

Added .id
2016-06-16 15:26:39 -04: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
Rohan Desai
8cc87f3858 Merge pull request #966 from rohandesai/PENG-160
PENG-160 Alfred workflow should not allow undefined commands
2016-06-13 14:17:10 -07:00
Vinay Rao
e818695947 Merge pull request #987 from SmartThingsCommunity/staging
Roll down hotfix
2016-06-13 10:50:29 -07:00
Vinay Rao
bbba20288e Merge pull request #986 from larsfinander/DVCSMP-1819_Philips_Hue_Incorrect_folder_name_for_Ambiance_bulb_DTH_staging
DVCSMP-1819 Philips Hue: Incorrect folder name for Ambiance bulb DTH
2016-06-13 10:49:34 -07:00
Lars Finander
ff9dd3f6e2 DVCSMP-1819 Philips Hue: Incorrect folder name for Ambiance bulb DTH
-Renamed folder to .src
2016-06-13 10:46:25 -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
7adff88d0f Merge pull request #978 from SmartThingsCommunity/master
Rolling up master to staging
2016-06-07 16:47:52 -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
Rohan Desai
49d293e749 PENG-160 UBI should not allow undefined commands
- added some more changes

added changes to alfred workflow
2016-06-03 15:25:50 -07:00
6 changed files with 189 additions and 285 deletions

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,244 +0,0 @@
/**
* Copyright 2016 SmartThings
*
* 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: "Z-Wave Plus Window Shade", namespace: "smartthings", author: "SmartThings") {
capability "Window Shade"
capability "Battery"
capability "Refresh"
capability "Health Check"
capability "Actuator"
capability "Sensor"
command "stop"
capability "Switch Level" // until we get a Window Shade Level capability
capability "Switch" // temporary for use with Routines
// This device handler is specifically for position-aware window coverings
//
fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Shade"
fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Shade"
fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Window Blinds"
fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Roller Shade"
}
simulator {
status "open": "command: 2603, payload: FF"
status "closed": "command: 2603, payload: 00"
status "10%": "command: 2603, payload: 0A"
status "66%": "command: 2603, payload: 42"
status "99%": "command: 2603, payload: 63"
status "battery 100%": "command: 8003, payload: 64"
status "battery low": "command: 8003, payload: FF"
// reply messages
reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE"
reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE"
reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE"
reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE"
}
tiles(scale: 2) {
multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") {
attributeState "open", label:'${name}', action:"close", icon:"st.doors.garage.garage-open", backgroundColor:"#79b821", nextState:"closing"
attributeState "closed", label:'${name}', action:"open", icon:"st.doors.garage.garage-closed", backgroundColor:"#ffffff", nextState:"opening"
attributeState "partiallyOpen", label:'Open', action:"close", icon:"st.doors.garage.garage-open", backgroundColor:"#79b821", nextState:"closing"
attributeState "opening", label:'${name}', action:"stop", icon:"st.doors.garage.garage-opening", backgroundColor:"#79b821", nextState:"partiallyOpen"
attributeState "closing", label:'${name}', action:"stop", icon:"st.doors.garage.garage-closing", backgroundColor:"#ffffff", nextState:"partiallyOpen"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"setLevel"
}
}
standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled"
state "disabled", label:'', action:"", icon:"st.secondary.refresh"
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
}
preferences {
input "preset", "number", title: "Default half-open position (1-100)", defaultValue: 50, required: false, displayDuringSetup: false
}
main(["windowShade"])
details(["windowShade", "refresh", "battery"])
}
}
def parse(String description) {
def result = null
//if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/)
// TODO: Workaround manual parsing of v4 multilevel report
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value
if (cmd) {
result = zwaveEvent(cmd)
}
log.debug "Parsed '$description' to ${result.inspect()}"
return result
}
def getCheckInterval() {
// These are battery-powered devices, and it's not very critical
// to know whether they're online or not 12 hrs
12 * 60 * 60
}
def installed() {
sendEvent(name: "checkInterval", value: checkInterval, displayed: false)
}
def updated() {
if (device.latestValue("checkInterval") != checkInterval) {
sendEvent(name: "checkInterval", value: checkInterval, displayed: false)
}
if (!device.latestState("battery")) {
response(zwave.batteryV1.batteryGet())
}
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
handleLevelReport(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
handleLevelReport(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
handleLevelReport(cmd)
}
private handleLevelReport(physicalgraph.zwave.Command cmd) {
def descriptionText = null
def shadeValue = null
def level = cmd.value as Integer
if (level >= 99) {
level = 99
shadeValue = "open"
} else if (level <= 0) {
level = 0 // unlike dimmer switches, the level isn't saved when closed
shadeValue = "closed"
} else {
shadeValue = "partiallyOpen"
descriptionText = "${device.displayName} shade is ${level}% open"
}
def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false)
def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: levelEvent.isStateChange)
def result = [stateEvent, levelEvent]
if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) {
log.debug "requesting battery"
state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row
result << response(["delay 15000", zwave.batteryV1.batteryGet().format()])
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) {
[ createEvent(name: "windowShade", value: "partiallyOpen", displayed: false, descriptionText: "$device.displayName shade stopped"),
response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ]
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
if (cmd.manufacturerName) {
updateDataValue("manufacturer", cmd.manufacturerName)
}
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
state.lastbatt = now()
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.debug "unhandled $cmd"
return []
}
def open() {
log.debug "open()"
/*delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)*/
zwave.basicV1.basicSet(value: 0xFF).format()
}
def close() {
log.debug "close()"
/*delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)*/
zwave.basicV1.basicSet(value: 0).format()
}
def on() {
open()
}
def off() {
close()
}
def setLevel(value, duration = null) {
log.debug "setLevel(${value.inspect()})"
Integer level = value as Integer
if (level < 0) level = 0
if (level > 99) level = 99
if (!preset && level > 0 && level < 95) state.preset = level
delayBetween([
zwave.basicV1.basicSet(value: level).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
])
}
def presetPosition() {
setLevel(preset ?: state.preset ?: 50)
}
def stop() {
log.debug "stop()"
zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format()
}
def ping() {
zwave.switchMultilevelV1.switchMultilevelGet().format()
}
def refresh() {
log.debug "refresh()"
delayBetween([
zwave.switchMultilevelV1.switchMultilevelGet().format(),
zwave.batteryV1.batteryGet().format()
], 1500)
}

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())

View File

@@ -658,29 +658,73 @@ def updateDevice() {
def data = request.JSON
def command = data.command
def arguments = data.arguments
log.debug "updateDevice, params: ${params}, request: ${data}"
if (!command) {
render status: 400, data: '{"msg": "command is required"}'
} else {
def device = allDevices.find { it.id == params.id }
if (device) {
if (device.hasCommand("$command")) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Command not supported by this Device"}'
}
} else {
render status: 404, data: '{"msg": "Device not found"}'
}
if (device) {
if (validateCommand(device, command)) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}'
}
} else {
render status: 404, data: '{"msg": "Device not found"}'
}
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(device)
if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) {
return command in capabilityCommands[currentDeviceCapability] ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(device) {
def capName = ""
if (switches.find{it.id == device.id})
capName = "Switch"
else if (alarms.find{it.id == device.id})
capName = "Alarm"
else if (locks.find{it.id == device.id})
capName = "Lock"
log.trace "Device: $device - Capability Name: $capName"
return capName
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
def listSubscriptions() {
log.debug "listSubscriptions()"
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
@@ -780,17 +824,51 @@ def deviceHandler(evt) {
def sendToHarmony(evt, String callbackUrl) {
def callback = new URI(callbackUrl)
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
sendHubCommand(new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [
"Host": host,
"Content-Type": "application/json"
],
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
))
if(isIP(callback.host)){
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
sendHubCommand(new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [
"Host": host,
"Content-Type": "application/json"
],
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
))
} else {
def params = [
uri: callbackUrl,
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
]
try {
log.debug "Sending data to Harmony Cloud: $params"
httpPostJson(params) { resp ->
log.debug "Harmony Cloud - Response: ${resp.status}"
}
} catch (e) {
log.error "Harmony Cloud - Something went wrong: $e"
}
}
}
public static boolean isIP(String str) {
try {
String[] parts = str.split("\\.");
if (parts.length != 4) return false;
for (int i = 0; i < 4; ++i) {
int p
try {
p = Integer.parseInt(parts[i]);
} catch (Exception e) {
return false;
}
if (p > 255 || p < 0) return false;
}
return true;
} catch (Exception e) {
return false;
}
}
def listHubs() {

View File

@@ -92,22 +92,87 @@ void updateLock() {
private void updateAll(devices) {
def command = request.JSON?.command
if (command) {
devices."$command"()
def type = params.param1
if (!devices) {
httpError(404, "Devices not found")
}
if (command){
devices.each { device ->
executeCommand(device, type, command)
}
}
}
private void update(devices) {
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
def command = request.JSON?.command
if (command) {
def device = devices.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
device."$command"()
}
def type = params.param1
def device = devices?.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
}
if (command) {
executeCommand(device, type, command)
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, deviceType, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(deviceType)
if (capabilityCommands[currentDeviceCapability]) {
return command in capabilityCommands[currentDeviceCapability] ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(type) {
switch(type) {
case "switches":
return "Switch"
case "locks":
return "Lock"
default:
return type
}
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
/**
* Validates and executes the command
* on the device or devices
*/
def executeCommand(device, type, command) {
if (validateCommand(device, type, command)) {
device."$command"()
} else {
httpError(403, "Access denied. This command is not supported by current capability.")
}
}
private show(devices, name) {