Compare commits

..

12 Commits

Author SHA1 Message Date
Nicolas Neverov
04e098603d Modifying 'Blue Iris Integration intranet' 2017-04-30 06:11:00 -07:00
Nicolas Neverov
7b683677d1 MSA-1939: BlueIris is a well known security camera management software.
This App+Handler bundle allows to integrate it with Samsung SmartThings, allowing to to manage Blue Iris signal and profile state via SmartThings app - either manually or automatically, by subscribing to location events (like Home, Away, etc).
The main difference with existing integration (by @pursual) is that this one works locally, thus eliminating need to create a potential security breach by exposing ports to internet (it works without any port forwarding). Of course it means BlueIris server should reside on the same network segment as SmartThings Hub.
2017-04-30 06:07:00 -07:00
Vinay Rao
7e8baeeb0b Merge pull request #1948 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2017-04-25 15:57:52 -07:00
Stephen Stack
ad824a9dd8 Merge pull request #1934 from SmartThingsCommunity/multi-sensor-accel-fix
DVCSMP-2573: Acceleration axis validation (Multi sensor DTH)
2017-04-25 15:11:54 -05:00
Stephen Stack
980bef6879 DVCSMP-2573: Acceleration axis validation (Multi sensor DTH)
In certain cases the SmartSense Multi Sensors are
missing the Y and Z axis, causing an exception
during .parseAxis(). This change checks that all
3 axis are present before processing the rest of
the message.
2017-04-24 16:51:03 -05:00
Zach Varberg
120935f14e Merge pull request #1940 from varzac/sengled-button-level-fix
[DVCSMP-2597] Fix sengled use of FF for max level
2017-04-24 16:01:36 -05:00
Jack Chi
032f4a92d4 Merge pull request #1937 from parijatdas/smartalert_siren
[CHF-590] Health Check smartalert-siren
2017-04-25 01:58:13 +09:00
Jack Chi
6d528683e6 Merge pull request #1904 from pchomal/plantlink_hc
[CHF-561] Health Check Plant Link
2017-04-25 01:51:24 +09:00
Zach Varberg
8bcbe7b924 Fix sengled use of FF for max level
This works around the fact that sengled element touches can get back
into a state of reporting an invalid value (of FF) if the physical
button on the bulb is used to cycle back to the 100% state.  Here we
interpret FF as FE for sengled and then issue a move to level command to
put it in a state where it will report FE until the level is changed
again.

This resolves: https://smartthings.atlassian.net/browse/DVCSMP-2597
2017-04-24 09:49:02 -05:00
Parijat Das
69dd13f333 Added healthcheck for SmartAlert Siren 2017-04-24 12:36:53 +05:30
piyush.c
29c7049d60 [CHF-561] Health Check Plant Link 2017-04-21 10:35:55 +05:30
Vinay Rao
03a79b8bb5 Merge pull request #1926 from SmartThingsCommunity/staging
Rolling down staging to master
2017-04-20 01:41:42 -07:00
12 changed files with 1077 additions and 15 deletions

View File

@@ -0,0 +1,559 @@
/**
* Copyright 2015 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.
*
* BlueIris (LocalConnect2)
*
* Author: Nicolas Neverov
* Date: 2017-04-30
*/
metadata {
definition (name: "blueiris2", namespace: "df", author: "df") {
capability "Sensor"
capability "Actuator"
capability "Configuration"
capability "Refresh"
attribute "state", "enum", ["disarmed", "arming", "armed", "disarming", "unknown"]
attribute "status", "string"
command "arm"
command "disarm"
command "location" "STRING"
command "retry"
command "timeout"
}
// simulator metadata
simulator {
}
preferences {
input name:"username", type:"text", title: "Username", description: "BlueIris Username", required: true
input name:"password", type:"password", title: "Password", description: "BlueIris Password", required: true
}
// UI tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"bi_detail_tile", type:"generic", width:6, height:4) {
tileAttribute("device.state", key: "PRIMARY_CONTROL") {
attributeState "disarmed", label:"Disarmed", action:"arm", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
attributeState "arming", label:"Arming...", action:"arm", icon:"st.locks.lock.locked", backgroundColor:"#79b821"
attributeState "armed", label:"Armed", action:"disarm", icon:"st.locks.lock.locked", backgroundColor:"#79b821"
attributeState "disarming", label:"Disarming...", action:"disarm", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
attributeState "unknown", label:"Unknown", action:"retry", icon:"st.locks.lock.unknown", backgroundColor:"#ff0000"
attributeState "refreshing", action:"Refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff"
}
tileAttribute("device.status", key: "SECONDARY_CONTROL") {
attributeState("default", label:'${currentValue}', defaultState:true)
}
}
standardTile("bi_refresh_tile", "device.refresh", decoration: "flat", width: 2, height: 2) {
state "default", action:"Refresh.refresh", icon:"st.secondary.refresh"
}
main "bi_detail_tile"
details(["bi_detail_tile", "bi_refresh_tile"])
}
}
def fsmExecInternal(fsmDefinition, stateId, Map params)
{
def actionResult = null;
def fsmState = fsmDefinition[stateId?.toString()]
if(fsmState == null || fsmState.isFinal) {
return [error:"fsmExecInternal: state [$stateId] ${fsmState == null ? 'does not exist' : 'is final'} and cannot be actioned upon"]
}
while(!fsmState.isFinal && !actionResult?.isAsync) {
actionResult = fsmState.action(params)
fsmState = fsmDefinition[actionResult.nextStateId?.toString()]
if(fsmState == null) {
return [error:"fsmExecInternal: state [${actionResult.nextStateId}] does not exist and cannot be actioned upon"]
}
log.debug("fsmExecInternal: transitioned state [$stateId] to [$actionResult.nextStateId] (isFinal:${fsmState.isFinal?:false}); result isAsync:${actionResult.isAsync?:false}")
stateId = actionResult.nextStateId
}
return [actionResult:actionResult]
}
def fsmGetStateId(Map persistentStg)
{
persistentStg.fsmState
}
def fsmExec(Map persistentStg, fsmDefinition, Map params = null)
{
def stateId = fsmGetStateId(persistentStg) ?: params.fsmInitialState
if(!stateId) {
return [error: "fsmExec: cannot determine initial state, must be specified via params.fsmInitialState"]
}
log.debug("fsmExec: ${persistentStg.fsmState ? 'proceeding' : 'starting'} with fsm state [$stateId]")
params = (params != null ? params : [:])
params.persistentStg = (params.persistentStg != null ? params.persistentStg : persistentStg)
def rc = fsmExecInternal(fsmDefinition, stateId, params)
if(rc.actionResult) {
persistentStg.fsmState = rc.actionResult.nextStateId
}
return rc
}
def getBlueIrisHubAction(Map body)
{
final host = getHostAddress();
final path = "/json"
def hubAction = new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [HOST:host],
body: body
)
log.info "getBlueIrisHubAction: prepared hubaction $hubAction, requestId: $hubAction.requestId"
hubAction
}
def sendError(statusMsg)
{
sendEvent(name:"status", value: statusMsg)
sendEvent(name:"state", value: "unknown")
parent.onNotification("$device.displayName: $statusMsg");
}
def sendEventInit(cmd)
{
def state;
switch(cmd.id) {
case 'status':
state = 'unknown'
break
case 'set':
state = (cmd.workflow == 'arm' ? 'arming' : 'disarming')
break
default:
state = 'unknown' //TODO: error
}
sendEvent(name:"state", value: state)
sendEvent(name:"status", value: "Logging in...")
}
def sendEventLogin(cmd)
{
sendEvent(name:"status", value: "Opening session...")
}
def sendEventStatus(cmd)
{
sendEvent(name:"status", value: "Getting status...")
}
def sendEventSet(cmd)
{
log.debug("sendEventSet: executing ${cmd.workflow} workflow, ${cmd.context ? ('context: ' + cmd.context + ',') : ''} signal:${cmd.signal}, profile:${cmd.profile}")
sendEvent(name:"status", value: "Executing '${cmd.workflow.toString() == 'arm' ? 'arm' : 'disarm'}${cmd.context ? ' ' + cmd.context : ''}' command")
}
def sendEventFinalize(cmd, signal, profile, workflow)
{
def isArmed = (workflow.toString() == 'arm')
def statusMsg = null
def statusDetails = null
if(cmd.id == 'set') {
statusMsg = "Successfully ${isArmed ? 'armed' : 'disarmed'}${cmd.context ? ' \'' + cmd.context + '\'' : ''} ..."
statusDetails = "Successfully ${isArmed ? 'armed' : 'disarmed'} ${cmd.context ? '\'' + cmd.context + '\'' : ''}"
} else if(cmd.id == 'status') {
statusMsg = "Status is '${isArmed ? 'Armed' : 'Disarmed'}'"
statusDetails = "Current status is '${isArmed ? 'Armed' : 'Disarmed'}' [signal:${signal ? 'green' : 'red'}, profile:${profile}]"
}
sendEvent(name:"state", value: isArmed ? "armed" : "disarmed")
sendEvent(name:"status", value: statusMsg)
parent.onNotification("${device.displayName}: ${statusDetails}");
}
def getBIfFsmDef()
{
[
init: [
action: {Map params ->
def cmd = params.cmd
params.persistentStg.cmd = params.cmd
sendEventInit(cmd)
parent.onBeginAsyncOp(getOperationTimeoutMs())
def haction = getBlueIrisHubAction([cmd: 'login']);
cmd.requestId = haction.requestId
[nextStateId:'login', isAsync:true, hubAction:haction]
}
],
login: [
action: {Map params ->
def cmd = params.persistentStg.cmd
def respData = params.respMsg.data
if(respData?.result == 'fail' && respData.session) {
sendEventLogin(cmd)
final u = settings.username
final p = settings.password
final token = "$u:${respData.session}:$p"
log.debug("getBIfFsmDef[login]: logging in user \"$u\"")
def haction = getBlueIrisHubAction([cmd: 'login', session: "${respData.session}", response: "${token.encodeAsMD5()}"]);
cmd.requestId = haction.requestId
cmd.session = respData.session
[nextStateId: (cmd.id == 'set' ? 'set' : 'status'), isAsync:true, hubAction: haction]
} else {
log.error("getBIfFsmDef[login]: error: unexpected result from login call: $params.respMsg.data")
parent.onEndAsyncOp()
sendError("Error logging in: unexpected result $params.respMsg.data?.result")
[nextStateId: 'error']
}
}
],
status: [
action: {Map params ->
def cmd = params.persistentStg.cmd
def respData = params.respMsg.data
if(respData?.result == 'success') {
sendEventStatus(cmd)
def haction = getBlueIrisHubAction([cmd: 'status', session: "${cmd.session}"]);
cmd.requestId = haction.requestId
[nextStateId: 'finalize', isAsync:true, hubAction: haction]
} else {
parent.onEndAsyncOp()
log.error("getBIfFsmDef[status]: error creating session: unsuccessful result from session login call: ${respData}");
sendError("Error establishing session: ${respData?.data?.reason}")
[nextStateId:'error']
}
}
],
set: [
action: {Map params ->
def cmd = params.persistentStg.cmd
def respData = params.respMsg.data
if(respData?.result == 'success') {
sendEventSet(cmd)
def hubActionParams = [cmd: 'status', session: "${cmd.session}"]
if(cmd.signal != null) {
hubActionParams.signal = cmd.signal ? 1 : 0
}
if(cmd.profile != null) {
hubActionParams.profile = cmd.profile
}
def haction = getBlueIrisHubAction(hubActionParams);
cmd.requestId = haction.requestId
[nextStateId:'finalize', isAsync:true, hubAction: haction]
} else {
parent.onEndAsyncOp()
log.error("getBIfFsmDef[action]: error creating session: unsuccessful result from session login call: ${respData}");
sendError("Error establishing session: ${respData.data?.reason}")
[nextStateId:'error']
}
}
],
finalize: [
action: {Map params ->
boolean success = false
final cmd = params.persistentStg.cmd
final respData = params.respMsg.data
if(respData?.result == 'success') {
def respSignal = (respData.data?.signal != null ? respData.data.signal == '1' : null);
def respProfile = respData.data?.profile
log.debug("getBIfFsmDef[finalize]: received 'success' response status, finalizing command ${cmd}, "
+ "response: [signal:${respSignal}, profile:${respProfile}]")
if( cmd.id == 'set'
&& (cmd.signal == null || cmd.signal == respSignal)
&& (cmd.profile == null || respProfile == cmd.profile.toString())) {
sendEventFinalize(cmd, respSignal, respProfile, cmd.workflow)
success = true
} else if(cmd.id == 'status' && respSignal != null && respProfile != null) {
def config = parent.onGetConfig()
def workflow = deduceWorkflow(respSignal, respProfile, config)
sendEventFinalize(cmd, respSignal, respProfile, workflow)
success = true
}
}
parent.onEndAsyncOp()
if(success) {
[nextStateId:'success']
} else {
log.error("getBIfFsmDef[finalize]: error setting status: unsuccessful result from setting status/signal: $params.respMsg.data");
sendError("Error setting status/signal: ${respData.data?.reason ?: 'signal mismatch...' }")
[nextStateId:'error']
}
}
],
timeout: [
action: {Map params ->
log.error("getBIfFsmDef[timeout]: operation timed out")
sendError("Operation timed out...")
[nextStateId:'error']
}
],
success: [
isFinal:true
],
error: [
isFinal:true
]
]
}
def configure()
{
log.debug("configure: reseting states")
sendEvent(name:"status", value: ".")
sendEvent(name:"state", value: "disarmed") //TODO: initial state calc
}
private getBIStg(boolean reset = false)
{
if (state.biStg == null || reset) {
state.biStg = [:]
}
state.biStg
}
private Map getBICommands()
{
return [
set: {Map p ->
def rc = [
id: 'set',
signal: p.signal,
profile: p.profile,
workflow: p.workflow //one of ['arm', 'disarm']
]
if (p.context != null) {
rc.context = p.context //optional (location mode name)
}
rc
},
status: {->
return [
id: 'status',
status: true
]
}
]
}
//signal: null (N/A), true(green), false(red)
//profile: null (N/A), number
private deduceWorkflow(Boolean signal, String profile, final config)
{
if ((signal != null && !signal) ||
(config.arming?.disarm?.signal == signal && config.arming?.disarm?.profile.toString() == profile)) {
"disarm"
} else {
"arm"
}
}
def arm()
{
log.debug('arm: running fsm')
def config = parent.onGetConfig()
log.debug("arm: retrieved config: $config")
def armConfig = config.arming?.arm
if(armConfig) {
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
signal:armConfig.signal, profile:armConfig.profile, workflow:'arm')])
if(rc.error || !rc.actionResult.isAsync) {
log.error("arm: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else {
return rc.actionResult.hubAction
}
} else {
log.error("arm: missing configuration (arming/arm) in $config")
// TODO: check return
}
}
def disarm()
{
log.debug('disarm: running fsm')
def config = parent.onGetConfig();
log.debug("disarm: retrieved config: $config")
def disarmConfig = config.arming?.disarm
if(disarmConfig) {
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
signal:disarmConfig.signal, profile:disarmConfig.profile, workflow:'disarm')])
if(rc.error || !rc.actionResult.isAsync) {
log.error("disarm: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else {
return rc.actionResult.hubAction
}
} else {
log.error("disarm: missing configuration (arming/arm) in $config")
// TODO: check return
}
}
def location(locationId)
{
def config = parent.onGetConfig()
log.debug("location: retrieved config: $config, processing location $locationId")
def locationConfig = config.location ? config.location[locationId] : null
if(locationConfig) {
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
signal: locationConfig.signal, profile: locationConfig.profile,
workflow: deduceWorkflow(locationConfig.signal, locationConfig.profile, config), context: locationConfig.name)])
if(rc.error || !rc.actionResult.isAsync) {
log.error("location: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else {
return rc.actionResult.hubAction
}
} else {
log.debug("location: no active configuration found for locationId:${locationId}' in configuration [$config]")
// TODO: check return
}
}
def retry()
{
def stg = getBIStg()
if(stg.cmd != null) {
log.debug("retry: running fsm with cmd:$stg.cmd")
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:stg.cmd])
if(rc.error || !rc.actionResult.isAsync) {
log.error("retry: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else {
return rc.actionResult.hubAction
}
} else {
log.debug("retry: no state to retry")
}
}
def timeout()
{
def stg = getBIStg()
log.debug("timeout: running fsm with cmd:${stg.cmd}")
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'timeout', cmd:stg.cmd])
if(rc.error || rc.actionResult.isAsync) {
log.error("timeout: error executing fsm: ${rc.error ?: 'expected synchronious action result'}")
}
}
def refresh()
{
log.debug('refresh: running fsm: status command')
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().status()])
if(rc.error || !rc.actionResult.isAsync) {
log.error("refresh: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else {
return rc.actionResult.hubAction
}
}
def parse(msg)
{
def lanMsg = parseLanMessage(msg)
log.info "parse: parsed lan message: $lanMsg"
if (lanMsg && lanMsg.headers && lanMsg.body) {
log.info "parse: parsed lan message requestId:$lanMsg.requestId, body:$lanMsg.body"
def stg = getBIStg()
if(fsmGetStateId(stg) && stg.cmd.requestId == lanMsg.requestId) {
log.debug("parse: received expected response mesage; requestId:$lanMsg.requestId, state:${fsmGetStateId(stg)}")
def rc = fsmExec(stg, getBIfFsmDef(), [respMsg:lanMsg])
if(rc.error) {
log.error("parse: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
} else if(rc.actionResult.isAsync) {
return [rc.actionResult.hubAction]
}
} else {
log.error("parse: skipping message: request id does not match: stg.request_id:$stg.cmd.requestId, lanMsg.requestId:$lanMsg.requestId")
}
} else {
log.error("parse: skipping message: unrecognized lan message")
}
}
private getOperationTimeoutMs()
{
10 * 1000;
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def parts = device.deviceNetworkId.split(":")
def ip = convertHexToIP(parts[0])
def port = convertHexToInt(parts[1])
return ip + ":" + port
}
private hashMD5(String somethingToHash) {
java.security.MessageDigest.getInstance("MD5").digest(somethingToHash.getBytes("UTF-8")).encodeHex().toString()
}
private calcDigestAuth(String method, String uri) {
def HA1 = hashMD5("${getUsername}::${getPassword}")
def HA2 = hashMD5("${method}:${uri}")
def response = hashMD5("${HA1}::::auth:${HA2}")
'Digest username="'+ getUsername() + '", realm="", nonce="", uri="'+ uri +'", qop=auth, nc=, cnonce="", response="' + response + '", opaque=""'
}

View File

@@ -0,0 +1,2 @@
.st-ignore
README.md

View File

@@ -0,0 +1,33 @@
# Osotech Plant Link
Cloud Execution
Works with:
* [OSO Technologies PlantLink Soil Moisture Sensor](https://www.smartthings.com/works-with-smartthings/oso-technologies/oso-technologies-plantlink-soil-moisture-sensor)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Troubleshooting](#troubleshooting)
## Capabilities
* **Sensor** - detects sensor events
* **Health Check** - indicates ability to get device health notifications
## Device Health
Plant Link sensor is a ZigBee sleepy device and checks in every 15 minutes.
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
* __32min__ checkInterval
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range.
Pairing needs to be tried again by placing the sensor closer to the hub.
Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links
for the different models:
* [OSO Technologies PlantLink Soil Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206868986-PlantLink-Soil-Moisture-Sensor)

View File

@@ -24,6 +24,7 @@ import groovy.json.JsonBuilder
metadata {
definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") {
capability "Sensor"
capability "Health Check"
command "setStatusIcon"
command "setPlantFuelLevel"
@@ -70,6 +71,16 @@ metadata {
}
}
def updated() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
def installed() {
// Device-Watch allows 2 check-in misses from device
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
def setStatusIcon(value){
def status = ''
switch (value) {
@@ -161,4 +172,4 @@ def parseDescriptionAsMap(description) {
map += []
}
}
}
}

View File

@@ -21,7 +21,7 @@ Works with:
## Device Health
Plant Link sensor is a Z-wave sleepy device and checks in every 15 minutes.
Plant Link sensor is a ZigBee sleepy device and checks in every 15 minutes.
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
* __32min__ checkInterval

View File

@@ -0,0 +1,2 @@
.st-ignore
README.md

View File

@@ -0,0 +1,39 @@
# Smartalert Siren
Cloud Execution
Works with:
* [FortrezZ Siren Strobe Alarm](https://www.smartthings.com/works-with-smartthings/other/fortrezz-water-valve)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Troubleshooting](#troubleshooting)
## Capabilities
* **Actuator** - represents that a Device has commands
* **Switch** - can detect state (possible values: on/off)
* **Sensor** - detects sensor events
* **Alarm** - allows for interacting with devices that serve as alarms
* **Health Check** - indicates ability to get device health notifications
## Device Health
FortrezZ Siren Strobe Alarm is polled by the hub.
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
* __32min__ checkInterval
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
* [FortrezZ Siren Strobe Alarm Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202294760-FortrezZ-Siren-Strobe-Alarm)

View File

@@ -21,10 +21,12 @@ metadata {
capability "Switch"
capability "Sensor"
capability "Alarm"
capability "Health Check"
command "test"
fingerprint deviceId: "0x1100", inClusters: "0x26,0x71"
fingerprint mfr:"0084", prod:"0313", model:"010B", deviceJoinName: "FortrezZ Siren Strobe Alarm"
}
simulator {
@@ -68,6 +70,16 @@ metadata {
}
}
def installed() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}
def updated() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}
def on() {
[
zwave.basicV1.basicSet(value: 0xFF).format(),
@@ -149,3 +161,10 @@ def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd)
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "UNEXPECTED COMMAND: $cmd"
}
/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
secure(zwave.basicV1.basicGet())
}

View File

@@ -191,6 +191,10 @@ private List<Map> parseAxis(List<Map> attrData) {
def y = hexToSignedInt(attrData.find { it.attrInt == 0x0013 }?.value)
def z = hexToSignedInt(attrData.find { it.attrInt == 0x0014 }?.value)
if ([x, y ,z].any { it == null }) {
return []
}
def xyzResults = [:]
if (device.getDataValue("manufacturer") == "SmartThings") {
// This mapping matches the current behavior of the Device Handler for the Centralite sensors
@@ -371,6 +375,10 @@ def updated() {
}
private hexToSignedInt(hexVal) {
if (!hexVal) {
return null
}
def unsignedVal = hexToInt(hexVal)
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
}

View File

@@ -70,19 +70,27 @@ def parse(String description) {
else {
sendEvent(event)
}
}
else {
def cluster = zigbee.parse(description)
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
if (cluster.data[0] == 0x00){
} else {
def descMap = zigbee.parseDescriptionAsMap(description)
if (descMap && descMap.clusterInt == 0x0006 && descMap.commandInt == 0x07) {
if (descMap.data[0] == "00") {
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
else {
} else {
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
}
}
else {
} else if (device.getDataValue("manufacturer") == "sengled" && descMap && descMap.clusterInt == 0x0008 && descMap.attrInt == 0x0000) {
// This is being done because the sengled element touch incorrectly uses the value 0xFF for the max level.
// Per the ZCL spec for the UINT8 data type 0xFF is an invalid value, and 0xFE should be the max. Here we
// manually handle the invalid attribute value since it will be ignored by getEvent as an invalid value.
// We also set the level of the bulb to 0xFE so future level reports will be 0xFE until it is changed by
// something else.
if (descMap.value.toUpperCase() == "FF") {
descMap.value = "FE"
}
sendHubCommand(zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, "FE0000").collect { new physicalgraph.device.HubAction(it) }, 0)
sendEvent(zigbee.getEventFromAttrData(descMap.clusterInt, descMap.attrInt, descMap.encoding, descMap.value))
} else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}

View File

@@ -1,4 +1,4 @@
# Z-Wave Switch
# Z-Wave Lock
Cloud Execution
@@ -6,7 +6,6 @@ Works with:
* [Yale Key Free Touchscreen Deadbolt (YRD240)](https://www.smartthings.com/works-with-smartthings/yale/yale-key-free-touchscreen-deadbolt-yrd240)
## Table of contents
* [Capabilities](#capabilities)
@@ -41,5 +40,3 @@ If the device doesn't pair when trying from the SmartThings mobile app, it is po
Pairing needs to be tried again by placing the device closer to the hub.
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
* [General Z-Wave/ZigBee Yale Lock Troubleshooting](https://support.smartthings.com/hc/en-us/articles/205138400-How-to-connect-Yale-locks)

View File

@@ -0,0 +1,384 @@
/**
* Copyright 2015 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.
*
* BlueIris (LocalConnect2)
*
* Author: Nicolas Neverov
* Date: 2017-04-30
*/
definition(
name: "BlueIris (LocalConnect2)",
namespace: "df",
author: "df",
description: "BlueIris local integration",
category: "Safety & Security",
singleInstance: true,
iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.doors.garage.garage-closed",
iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.doors.garage.garage-closed?displaySize=2x"
)
preferences {
page(name: "setup", title: "Blue Iris Setup", content: "pageSetupCallback")
page(name: "mode", title: "Blue Iris Modes Setup", content: "renderModePage")
page(name: "validate", title: "Blue Iris Setup", content: "pageValidateCallback")
}
def switchHandler(evt)
{
log.debug "setupDevice: switch event: $evt.value"
}
private Map getValidators()
{
return [
hostAddress: { addr ->
return addr ==~ /\d+\.\d+\.\d+\.\d+(:\d+)?/
},
mode: { Map p ->
def rc = []
if (p.aProfileApply) {
if (p.aProfile == null) {
rc.push("Arming profile is required");
} else if (p.aProfile < 1 || p.aProfile > 5) {
rc.push("Arming profile must be within [1-5] range")
}
}
if (p.dProfileApply) {
if (p.dProfile == null) {
rc.push("Disarming profile is required");
} else if (p.dProfile < 1 || p.dProfile > 5) {
rc.push("Disarming profile must be within [1-5] range")
}
}
def armProfile = p.aProfileApply ? p.aProfile : 0;
def disarmProfile = p.dProfileApply ? p.dProfile : 0;
if (p.aSignal == p.dSignal && armProfile == disarmProfile && armProfile != null) {
rc.push("Arming and disarming signal/profile combinations must differ")
}
return rc
}
]
}
def pageSetupCallback()
{
if (canInstallLabs()) {
log.debug("pageSetupCallback: refreshing")
def v = getValidators()
return dynamicPage(name:"setup", title:"Setting up Blue Iris integration", nextPage:"", install: false, uninstall: true) {
section("New BlueIris Server setup") {
input name:"devicename", type:"text", title: "Device name", required:true, defaultValue: "Blue Iris Server"
input name:"hub", type:"hub", title: "Hub gateway", required:true
input name:"ip", type:"text", title: "IP address:port", required:true, submitOnChange:true
if (!v.hostAddress(ip)) {
paragraph(required:true, "Please specify valid IP address")
}
input name:"username", type:"text", title: "Username", required:true, autoCorrect:false
input name:"password", type:"password", title: "Password", required:true, autoCorrect:false
}
if(v.hostAddress(ip)) {
section("") {
href(title:"Next", description:"", page:"mode", required:true)
}
}
}
} else {
return dynamicPage(name:"setup", title:"Upgrade needed", nextPage:"", install:false, uninstall: true) {
section("Upgrade needed") {
paragraph "Hub firmware needs to be upgraded"
}
}
}
}
private makeProfileInput(inputName)
{
input(name:inputName.toString(), type:"number", title:"Select profile [1-5]:", range:"1..5", submitOnChange:true, required:true)
}
def renderModePage() {
def v = getValidators()
return dynamicPage(name:"mode", title:"Setting up Blue Iris modes", nextPage:"", install: false, uninstall: true) {
section(hideable:true, "Arming modes") {
input(name:"armSignal", type:"enum", title:"When Armed, set signal to", options:["Green","N/A"], defaultValue:"Green", submitOnChange:true, required:false)
input(name:"armProfileApply", type:"bool", title:"Also, change profile?", defaultValue:false, submitOnChange:true)
if (armProfileApply) {
makeProfileInput("armProfile")
}
input(name:"disarmSignal", type:"enum", title:"When Disarmed, set signal to", options: ["Red", "N/A"], defaultValue:"Red", submitOnChange:true, required:false)
input(name:"disarmProfileApply", type:"bool", title:"Also, change profile?", defaultValue:false, submitOnChange:true)
if (disarmProfileApply) {
makeProfileInput("disarmProfile")
}
}
section(hideable:true, "Location modes") {
location.modes.each {mode->
input(name:"locationSignal${mode.id}".toString(), type:"enum", title:"When in \"$mode.name\" mode, set signal to", options: ["Green", "Red", "N/A"], defaultValue:"N/A", required:false, submitOnChange:true)
input(name:"locationProfileApply${mode.id}".toString(), type:"bool", title:"Also, change profile?", defaultValue:false, required:false, submitOnChange:true)
if (settings["locationProfileApply${mode.id}".toString()] == true) {
makeProfileInput("locationProfile${mode.id}")
}
}
}
def p = [
aSignal: armSignal,
aProfileApply: armProfileApply,
aProfile: armProfile,
dSignal: disarmSignal,
dProfileApply: disarmProfileApply,
dProfile: disarmProfile
]
def valRc = v.mode(p)
if (valRc) {
section("Please correct errors:") {
valRc.each {err ->
paragraph(required:true, "*** $err")
}
}
} else {
section("") {
href(title:"Next", description:"", page:"validate", required:true)
}
}
}
}
def pageValidateCallback()
{
if(ip ==~ /\d+\.\d+\.\d+\.\d+(:\d+)?/) {
return dynamicPage(name:"validate", title:"Setting up Blue Iris integration", install:true, uninstall:false) {
section() {
paragraph(
image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
title:"Ready to install",
"Press 'Done' to confirm installation"
)
}
}
} else {
return dynamicPage(name:"validate", title:"Setting up Blue Iris", nextPage:"", install: false, uninstall:false) {
section("Error validating setup preferences") {
paragraph(
image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
title:"IP Address",
required:true,
"Should look similar to 111.222.333.555:8001 (port is optional)"
)
}
}
}
}
def installed()
{
log.debug("installed: started") //with $settings
init()
}
def updated()
{
log.debug("updated: started"); //with $settings
uninit();
init()
}
def uninstalled()
{
uninit(false);
}
def init()
{
if(!state.subscribed) {
subscribe(location, "mode", modeChangeHandler)
state.subscribed = true
}
state.config = assembleConfig()
final dni = ipEpToHex(ip)
def d = getChildDevice(dni)
if(d) {
log.debug("init: deleting existing BlueIris Server device, dni:$dni")
deleteChildDevice(dni)
}
if(true) {
log.debug "init: adding new BlueIris Server device, dni:$dni, username:$username, password:*****, gateway hub id:$hub.id"
d = addChildDevice("df", "blueiris2", dni, hub.id,
[name:"blueiris", label: devicename, completedSetup:true,
"preferences":["username":username, "password":password]
])
d.configure()
subscribe(d, "switch", switchHandler)
} else {
log.debug "init: skipping adding BlueIris Server device, dni:$dni - already exists"
}
}
def uninit(boolean f_unsubscribe = true)
{
if(state.subscribed) {
if(f_unsubscribe) {
unsubscribe()
}
getAllChildDevices().each {
}
state.subscribed = false
}
}
def modeChangeHandler(evt)
{
def f_arm = (evt.value == 'Away')
log.debug("modeChangeHandler: detected mode change: $evt.name:$evt.value: ${f_arm ? 'arming' : 'disarming'}")
def mode = null
location.modes.each {m->
if (m.name == evt.value) {
mode = m
}
}
getAllChildDevices().each {
it.location(mode.id)
}
}
def asyncOpCallback()
{
log.debug("asyncOpCallback: timeout:$atomicState.asyncOpTimeout, ${now() - atomicState.asyncOpTs}(msec) elapsed")
if(atomicState.asyncOpTimeout) {
getAllChildDevices().each {
it.timeout()
}
}
}
def onBeginAsyncOp(int timeout_ms)
{
log.debug("onBeginAsyncOp: ${now()}")
atomicState.asyncOpTimeout = true
atomicState.asyncOpTs = now()
runOnce(new Date(now() + timeout_ms), asyncOpCallback, [overwrite: true])
}
def onEndAsyncOp()
{
log.debug("onEndAsyncOp: ${now()}")
atomicState.asyncOpTimeout = false
runOnce(new Date(now() + 1), asyncOpCallback, [overwrite: true])
}
def onNotification(msg)
{
log.debug("sendNotification: sending $msg")
sendNotificationEvent(msg)
}
def onGetConfig()
{
return state.config
}
private assembleConfig()
{
def getElementCfg = {prefix, id, name ->
def signal = settings["${prefix}Signal${id}".toString()]
def profileApply = settings["${prefix}ProfileApply${id}".toString()]
def profile = settings["${prefix}Profile${id}".toString()]
return signal != 'N/A' || profileApply ? [
name: name,
signal: signal == 'N/A' ? null : (signal == "Green"),
profile: profileApply ? profile : null
] : null
}
def rc = [
arming: [
arm: getElementCfg('arm', '', 'Arm'),
disarm: getElementCfg('disarm', '', 'Disarm')
],
location: [:]
]
location.modes.each {mode->
rc.location["$mode.id".toString()] = getElementCfg('location', mode.id, mode.name)
}
log.info("onGetConfig: assembled config: [$rc]")
rc
}
private Boolean canInstallLabs()
{
return hasAllHubsOver("000.011.00603")
}
private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it }
}
private String ipEpToHex(ep) {
final parts = ep.split(':');
final ipHex = parts[0].tokenize('.').collect{ String.format('%02X', it.toInteger() ) }.join()
final portHex = String.format('%04X', (parts[1]?:80).toInteger())
return "$ipHex:$portHex"
}
private String hexToString(String txtInHex)
{
byte [] txtInByte = new byte [txtInHex.length() / 2];
int j = 0;
for (int i = 0; i < txtInHex.length(); i += 2)
{
txtInByte[j++] = Byte.parseByte(txtInHex.substring(i, i + 2), 16);
}
return new String(txtInByte);
}