├── smartapps
├── readme.md
├── pstuart
│ ├── minimote-set-security-mode.src
│ │ └── minimote-set-security-mode.groovy
│ ├── test-push-notification-v2.src
│ │ └── test-push-notification-v2.groovy
│ ├── test-live-video-trigger.src
│ │ └── test-live-video-trigger.groovy
│ ├── live-code-friday-dynamic-page-function-call-issue.src
│ │ └── live-code-friday-dynamic-page-function-call-issue.groovy
│ ├── live-code-hvac-fan-run-away.src
│ │ └── live-code-hvac-fan-run-away.groovy
│ └── live-code-friday-virtual-device-manager.src
│ │ └── live-code-friday-virtual-device-manager.groovy
├── GetPublicIP
├── set tstat to not away.groovy
├── dawn to dust motion dimmer
├── Control4 Send Command Test SmartApp.goovy
├── sample-webservice.groovy
├── smartthings
│ └── laundry-monitor.src
│ │ └── laundry-monitor.groovy
├── logger.groovy
└── Ubi (Connect)
├── devicetypes
├── readme.md
├── cschwer
│ └── http-post-example.src
│ │ └── http-post-example.groovy
├── pstuart
│ ├── unknown-zigbee-test-device.src
│ │ └── unknown-zigbee-test-device.groovy
│ ├── generic-switch.src
│ │ └── generic-switch.groovy
│ ├── generic-dimmer.src
│ │ └── generic-dimmer.groovy
│ ├── omnipresence.src
│ │ └── omnipresence.groovy
│ ├── insteon-moisture-local.src
│ │ └── insteon-moisture-local.groovy
│ ├── insteon-switch-local.src
│ │ └── insteon-switch-local.groovy
│ └── generic-camera.src
│ │ └── generic-camera.groovy
├── ps
│ ├── get-aws-ip.src
│ │ └── get-aws-ip.groovy
│ └── ps-cardaccess-zigbee-to-ir.src
│ │ └── ps-cardaccess-zigbee-to-ir.groovy
├── smartslots.groovy
├── GE Link Bulb.groovy
├── smartthings
│ ├── hue-bulb.src
│ │ └── hue-bulb.groovy
│ └── lifx-color-bulb.src
│ │ └── lifx-color-bulb.groovy
├── Test Temp Slider.groovy
├── Get Ubi Sensors
├── Control4 Zigbee HA Dimmer.groovy
├── nest.groovy
├── smartsense multi + graph.groovy
└── zenwithin
│ └── zen-thermostat-new-ui.src
│ └── zen-thermostat-new-ui.groovy
├── .gitignore
└── README.md
/smartapps/readme.md:
--------------------------------------------------------------------------------
1 | smartapps here
2 |
--------------------------------------------------------------------------------
/devicetypes/readme.md:
--------------------------------------------------------------------------------
1 | devicetypes here
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Rakefile
2 | devicestypes/pstuart/ps-test-html-tile.src/ps-test-html-tile.groovy
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | smartthings
2 | ===========
3 |
4 | Place to hold my files and revisions on smartthings apps and devices
5 |
6 |
7 | How to set up a custom device
8 |
9 |
10 |
11 | You need IDE access. https://graph.api.smartthings.com/
12 |
13 | go into my device types https://graph.api.smartthings.com/ide/devices
14 |
15 | click + New SmartDevice
16 |
17 | fill out just name, namespace and author and click create
18 |
19 | copy and paste the code
20 |
21 | Click the save button
22 |
23 | Click the publish button, for me
24 |
25 | Go to My Devices
26 |
27 | click + Add New Devices
28 |
29 | Give it the following:
30 | Name = anything you want
31 | device Network Id = unique id (should be the hex IP and hex port of the device but my code will auto insert that when you set the ip and take a photo)
32 | Type = Generic Camera Device
33 | Version = Published
34 | Location = your location
35 | hub = your hub
36 | group = what folder do you want the "thing" in, none is the default.
37 |
38 | then click create
39 |
--------------------------------------------------------------------------------
/devicetypes/cschwer/http-post-example.src/http-post-example.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * HTTP Post Example
3 | *
4 | * Copyright 2015 Charles Schwer
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | */
15 | metadata {
16 | definition (name: "HTTP Post Example", namespace: "cschwer", author: "Charles Schwer") {
17 | }
18 |
19 | simulator {
20 | }
21 |
22 | tiles {
23 | }
24 | }
25 |
26 | // Parse events into attributes
27 | def parse(String description) {
28 | log.debug "Parsing '${description}'"
29 | def msg = parseLanMessage(description)
30 |
31 | log.debug "data ${msg.data}"
32 | log.debug "body ${msg.body}"
33 | log.debug "headers ${msg.headers}"
34 | }
--------------------------------------------------------------------------------
/smartapps/pstuart/minimote-set-security-mode.src/minimote-set-security-mode.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * MiniMote Set Security Mode
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "MiniMote Set Security Mode",
18 | namespace: "pstuart",
19 | author: "Patrick Stuart",
20 | description: "MiniMote Set Security Mode",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
25 |
26 |
27 | preferences {
28 | section("MiniMote") {
29 | input "button", "capability.button"
30 | }
31 | }
32 |
33 | def installed() {
34 | log.debug "Installed with settings: ${settings}"
35 |
36 | initialize()
37 | }
38 |
39 | def updated() {
40 | log.debug "Updated with settings: ${settings}"
41 |
42 | unsubscribe()
43 | initialize()
44 | }
45 |
46 | def initialize() {
47 | // TODO: subscribe to attributes, devices, locations, etc.
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/devicetypes/pstuart/unknown-zigbee-test-device.src/unknown-zigbee-test-device.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Unknown Zigbee Test Device
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "Unknown Zigbee Test Device", namespace: "pstuart", author: "Patrick Stuart") {
18 | capability "Refresh"
19 | }
20 |
21 | simulator {
22 | // TODO: define status and reply messages here
23 | }
24 |
25 | tiles {
26 | // TODO: define your main and details tiles here
27 | }
28 | }
29 |
30 | // parse events into attributes
31 | def parse(String description) {
32 | log.debug "Parsing '${description}'"
33 |
34 | }
35 |
36 | // handle commands
37 | def refresh() {
38 | log.debug "Executing 'refresh'"
39 | // TODO: handle 'refresh' command
40 | getClusters();
41 | }
42 |
43 | def getClusters() {
44 | log.debug "getClusters hit $device.deviceNetworkId please check full logs for response, may need a smartapp installed that subscribes to location parsing, like SHM, it will not be filtered to this device in the format like cp desc: ep_cnt:4, ep:01 C4 C5 C6"
45 | "zdo active 0x${device.deviceNetworkId}"
46 | }
47 |
--------------------------------------------------------------------------------
/smartapps/pstuart/test-push-notification-v2.src/test-push-notification-v2.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Test Push Notification v2
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Test Push Notification v2",
18 | namespace: "pstuart",
19 | author: "Patrick Stuart",
20 | description: "Test Push Notification v2",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
25 |
26 |
27 | preferences {
28 | section("Send Notifications?") {
29 | input("recipients", "contact", title: "Send notifications to")
30 | }
31 | }
32 |
33 | def installed() {
34 | log.debug "Installed with settings: ${settings}"
35 |
36 | initialize()
37 | }
38 |
39 | def updated() {
40 | log.debug "Updated with settings: ${settings}"
41 |
42 | unsubscribe()
43 | initialize()
44 | }
45 |
46 | def initialize() {
47 | subscribe(app, appTouch)
48 | }
49 |
50 | def appTouch(evt) {
51 | sendNotificationToContacts("This is a test notification!", recipients)
52 | log.debug "Sent Notification"
53 |
54 | sendPush("This is a test sendPush notification.")
55 | log.debug "Sent sendPush Notification"
56 | }
--------------------------------------------------------------------------------
/smartapps/pstuart/test-live-video-trigger.src/test-live-video-trigger.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Test Live Video Trigger
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Test Live Video Trigger",
18 | namespace: "pstuart",
19 | author: "Patrick Stuart",
20 | description: "Test Live Video Trigger",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
25 |
26 |
27 | preferences {
28 | section("Pick Camera to Turn on/off") {
29 | input "camera", "capability.switch"
30 | }
31 | }
32 |
33 | def installed() {
34 | log.debug "Installed with settings: ${settings}"
35 |
36 | initialize()
37 | }
38 |
39 | def updated() {
40 | log.debug "Updated with settings: ${settings}"
41 |
42 | unsubscribe()
43 | initialize()
44 | }
45 |
46 | def initialize() {
47 | subscribe(app, appTouch)
48 | }
49 |
50 | def appTouch(evt) {
51 | log.debug "app touch hit"
52 | def cameraState = camera.currentValue("switch")
53 | log.debug cameraState
54 | //def cameraRecord = camera.currentValue("record")
55 | //log.debug cameraRecord
56 | if (cameraState.startsWith("off")) {
57 | camera.on()
58 | } else
59 | {
60 | camera.off() //turn off
61 | }
62 | }
--------------------------------------------------------------------------------
/devicetypes/pstuart/generic-switch.src/generic-switch.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Generic Switch
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "Generic Switch", namespace: "pstuart", author: "Patrick Stuart") {
18 | capability "Switch"
19 | }
20 |
21 | simulator {
22 | // TODO: define status and reply messages here
23 | }
24 |
25 | tiles {
26 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground:true)
27 | {
28 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#FF0000"
29 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00F208"
30 | }
31 | standardTile("toggle", "device.switch")
32 | {
33 | state "default", label:'Toggle', action: "toggle", icon: "st.secondary.refresh-icon"
34 | }
35 | main "switch"
36 | details(["switch","toggle"])
37 | }
38 | }
39 |
40 | // parse events into attributes
41 | def parse(String description) {
42 | log.debug "Parsing '${description}'"
43 | // TODO: handle 'switch' attribute
44 |
45 | }
46 |
47 | // handle commands
48 | def on() {
49 | log.debug "Executing 'on'"
50 | // TODO: handle 'on' command
51 | sendEvent(name: "switch", value: "on")
52 | }
53 |
54 | def off() {
55 | log.debug "Executing 'off'"
56 | // TODO: handle 'off' command
57 | sendEvent(name: "switch", value: "off")
58 | }
59 |
60 | def toggle() {
61 | log.debug "Executing Toggle"
62 | }
63 |
64 |
65 |
--------------------------------------------------------------------------------
/devicetypes/pstuart/generic-dimmer.src/generic-dimmer.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Generic Switch
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "Generic Dimmer", namespace: "pstuart", author: "Patrick Stuart") {
18 | capability "Switch"
19 | capability "Switch Level"
20 | }
21 |
22 | simulator {
23 | // TODO: define status and reply messages here
24 | }
25 |
26 | tiles {
27 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground:true)
28 | {
29 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#FF0000"
30 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00F208"
31 | }
32 | standardTile("toggle", "device.switch")
33 | {
34 | state "default", label:'Toggle', action: "toggle", icon: "st.secondary.refresh-icon"
35 | }
36 | main "switch"
37 | details(["switch","toggle"])
38 | }
39 | }
40 |
41 | // parse events into attributes
42 | def parse(String description) {
43 | log.debug "Parsing '${description}'"
44 | // TODO: handle 'switch' attribute
45 |
46 | }
47 |
48 | // handle commands
49 | def on() {
50 | log.debug "Executing 'on'"
51 | // TODO: handle 'on' command
52 | sendEvent(name: "switch", value: "on")
53 | }
54 |
55 | def off() {
56 | log.debug "Executing 'off'"
57 | // TODO: handle 'off' command
58 | sendEvent(name: "switch", value: "off")
59 | }
60 |
61 | def toggle() {
62 | log.debug "Executing Toggle"
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/devicetypes/ps/get-aws-ip.src/get-aws-ip.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Get AWS IP
3 | *
4 | * Copyright 2015 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 |
17 | preferences {
18 | }
19 |
20 | metadata {
21 | definition (name: "Get AWS IP", namespace: "ps", author: "patrick@patrickstuart.com") {
22 | capability "Polling"
23 | attribute "publicIp", "string"
24 | }
25 |
26 | simulator {
27 | }
28 |
29 | tiles(scale: 2) {
30 | valueTile("publicIp", "device.publicIp", inactiveLabel: false, decoration: "flat", width: 3, height: 2) {
31 | state "default", label:'${currentValue}', unit:"AWS IP"
32 | }
33 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
34 | state "default", action:"polling.poll", icon:"st.secondary.refresh"
35 | }
36 | valueTile("IPs", "device.ips", inactiveLabel: false, decoration: "flat", width: 6, height:7) {
37 | state "default", label:'${currentValue}', unit:"AWS IP List"
38 | }
39 |
40 | main "publicIp"
41 | details(["publicIp", "refresh", "IPs"])
42 | }
43 | }
44 |
45 | def parse(String description) {
46 | log.debug "Parsing '${description}'"
47 | }
48 |
49 | // handle commands
50 | def poll() {
51 | log.debug "Executing 'poll'"
52 | getIP()
53 | }
54 |
55 | def getIP() {
56 | def path = "/st/ip.php"
57 | def params = [
58 | uri: "http://valinor.net",
59 | path : path
60 | ]
61 | httpGet(params) { resp ->
62 | log.debug resp.data
63 | def publicip2 = resp.data.toString()
64 | sendEvent(name: 'publicIp', value: publicip2)
65 | def currentIps = device.currentValue("ips")
66 | if (!currentIps) { currentIps = "" }
67 | log.debug currentIps
68 | if (!currentIps.contains("$publicip2"))
69 | {
70 | currentIps += "\r\n" + publicip2
71 | }
72 | log.debug currentIps
73 | sendEvent(name: 'ips', value: currentIps)
74 | }
75 | }
--------------------------------------------------------------------------------
/devicetypes/pstuart/omnipresence.src/omnipresence.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * OmniPresence
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "OmniPresence", namespace: "pstuart", author: "Patrick Stuart", oauth: true) {
18 | capability "Presence Sensor"
19 | capability "Sensor"
20 | }
21 |
22 | simulator {
23 | status "present": "presence: 1"
24 | status "not present": "presence: 0"
25 | }
26 |
27 | tiles {
28 | standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
29 | state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
30 | state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
31 | }
32 | main "presence"
33 | details "presence"
34 | }
35 | }
36 |
37 | // parse events into attributes
38 | def parse(String description) {
39 | log.debug "Parsing '${description}'"
40 | def msg = parseLanMessage(description)
41 |
42 | log.debug "data ${msg.data}"
43 | log.debug "body ${msg.body}"
44 | log.debug "headers ${msg.headers}"
45 | if ( msg.headers.toString().contains("get /?present=true"))
46 | {
47 | def results = [
48 | name: "presence",
49 | value: "present",
50 | unit: null,
51 | linkText: "",
52 | descriptionText: "is Present",
53 | handlerName: null,
54 | isStateChange: true
55 | ]
56 | log.debug "Parse returned $results.descriptionText"
57 | return results
58 | } else if ( msg.headers.toString().contains("get /?present=false"))
59 | {
60 | def results = [
61 | name: "presence",
62 | value: "not present",
63 | unit: null,
64 | linkText: "",
65 | descriptionText: "is Not Present",
66 | handlerName: null,
67 | isStateChange: true
68 | ]
69 | log.debug "Parse returned $results.descriptionText"
70 | return results
71 | }
72 | }
--------------------------------------------------------------------------------
/devicetypes/smartslots.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * SmartSlots v1.0.0
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "SmartSlots v1.0", namespace: "pstuart", author: "Patrick Stuart") {
18 | command "spin"
19 | }
20 |
21 | simulator {
22 | }
23 |
24 | tiles {
25 | valueTile("slot1", "device.slot1", decoration:"flat") {
26 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:""
27 | }
28 | valueTile("slot2", "device.slot2", decoration:"flat") {
29 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:""
30 | }
31 | valueTile("slot3", "device.slot3", decoration:"flat") {
32 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:""
33 | }
34 | valueTile("slotResult", "device.slotResult", decoration:"flat") {
35 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:""
36 | }
37 | standardTile("Spin", "device.button") {
38 | state "Spin", label:'${name}', action:"spin", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902'
39 | }
40 |
41 | main "Spin"
42 | details(["slot1", "slot2", "slot3", "slotResult", "Spin"])
43 | }
44 | }
45 |
46 | // parse events into attributes
47 | def parse(String description) {
48 | log.debug "Parsing '${description}'"
49 |
50 | }
51 |
52 | def spin() {
53 | log.debug "Spin Hit"
54 | Random random = new Random()
55 | //log.debug random.nextInt(10)+1
56 | sendEvent(name: 'slot1', value: random.nextInt(10)+1)
57 | sendEvent(name: 'slot2', value: random.nextInt(10)+1)
58 | sendEvent(name: 'slot3', value: random.nextInt(10)+1)
59 | def slot1 = device.currentValue("slot1").toInteger()
60 | def slot2 = device.currentValue("slot2").toInteger()
61 | def slot3 = device.currentValue("slot3").toInteger()
62 |
63 | log.debug "$slot1 $slot2 $slot3 ${slot1 == slot2 && slot2 == slot3}"
64 |
65 | if (slot1 == slot2 && slot2 == slot3) {
66 | log.debug "Winner"
67 | sendEvent(name: 'slotResult', value: "Winner")
68 | } else {
69 | log.debug "Not a Winner"
70 | sendEvent(name: 'slotResult', value: "Not a Winner")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/smartapps/pstuart/live-code-friday-dynamic-page-function-call-issue.src/live-code-friday-dynamic-page-function-call-issue.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Live Code Friday Dynamic Page Function Call Issue
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Live Code Friday Dynamic Page Function Call Issue",
18 | namespace: "pstuart",
19 | author: "Patrick Stuart",
20 | description: "Live Code Friday Dynamic Page Function Call Issue",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
25 |
26 |
27 | preferences {
28 | page(name: "firstPage")
29 | page(name: "secondPage")
30 | }
31 |
32 | def firstPage() {
33 | dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
34 | section("Main Menu") {
35 | def test = testFunction("test1", "test2")
36 | paragraph "Testing A Functional Logging Issue from function $test"
37 | href(page: "secondPage", title: "Let's Add Devices!")
38 | }
39 | /*
40 | section("Later") {
41 | paragraph "More to come..."
42 | }
43 | */
44 |
45 | }
46 | }
47 |
48 | def secondPage() {
49 | dynamicPage(name: "secondPage", title: "Where to now?", install: true, uninstall: true) {
50 | section("Main Menu 2") {
51 | def test = testFunction("test1", "test2")
52 | paragraph "Testing A Functional Logging Issue from function $test"
53 | href(page: "secondPage", title: "Let's Add Devices!")
54 | }
55 | /*
56 | section("Later") {
57 | paragraph "More to come..."
58 | }
59 | */
60 |
61 | }
62 | }
63 |
64 | def testFunction(pOne, pTwo) {
65 | log.debug "This will not show up until after 'Done' is clicked"
66 | log.debug pOne
67 | log.debug pTwo
68 | return "test"
69 | }
70 |
71 |
72 | def installed() {
73 | log.debug "Installed with settings: ${settings}"
74 |
75 | initialize()
76 | }
77 |
78 | def updated() {
79 | log.debug "Updated with settings: ${settings}"
80 |
81 | unsubscribe()
82 | initialize()
83 | }
84 |
85 | def initialize() {
86 | // TODO: subscribe to attributes, devices, locations, etc.
87 | }
88 |
89 | // TODO: implement event handlers
--------------------------------------------------------------------------------
/smartapps/GetPublicIP:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_GetPublicIP
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 |
17 | preferences {
18 | }
19 |
20 | metadata {
21 | definition (name: "ps_GetPublicIP", namespace: "ps", author: "patrick@patrickstuart.com") {
22 | capability "Polling"
23 |
24 |
25 | attribute "publicIp", "string"
26 | }
27 |
28 | simulator {
29 |
30 | }
31 |
32 | tiles {
33 | valueTile("publicIp", "device.publicIp", inactiveLabel: false, decoration: "flat", columns:2) {
34 | state "default", label:'${currentValue}', unit:"Public IP"
35 | }
36 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat") {
37 | state "default", action:"polling.poll", icon:"st.secondary.refresh"
38 | }
39 |
40 |
41 | main "publicIp"
42 | details(["publicIp", "refresh"])
43 | }
44 | }
45 |
46 | // parse events into attributes
47 | def parse(String description) {
48 | log.debug "Parsing '${description}'"
49 | def map = stringToMap(description)
50 | def bodyString = new String(map.body.decodeBase64())
51 | log.debug bodyString
52 | def body = new XmlSlurper().parseText(bodyString)
53 | log.debug body
54 | def publicip2 = body.toString().replace("Current IP CheckCurrent IP Address: ","")
55 | log.debug publicip2
56 | sendEvent(name: 'publicIp', value: publicip2)
57 | }
58 |
59 | // handle commands
60 | def poll() {
61 | log.debug "Executing 'poll'"
62 | login()
63 | }
64 |
65 |
66 |
67 |
68 | def login() {
69 |
70 | def method = "GET"
71 | def host = "216.146.38.70"
72 | def hosthex = convertIPtoHex(host)
73 | def porthex = convertPortToHex(80)
74 | device.deviceNetworkId = "$hosthex:$porthex"
75 | def headers = [:]
76 | headers.put("HOST", "$host:80")
77 | def path = "/"
78 |
79 | def hubAction = new physicalgraph.device.HubAction(
80 | method: method,
81 | path: path,
82 | headers: headers
83 | )
84 | log.debug hubAction
85 | hubAction
86 | }
87 |
88 |
89 |
90 | private String convertIPtoHex(ipAddress) {
91 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
92 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
93 | return hex
94 | }
95 | private String convertPortToHex(port) {
96 | String hexport = port.toString().format( '%04x', port.toInteger() )
97 | log.debug hexport
98 | return hexport
99 | }
100 | private Integer convertHexToInt(hex) {
101 | Integer.parseInt(hex,16)
102 | }
103 | private String convertHexToIP(hex) {
104 | log.debug("Convert hex to ip: $hex")
105 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
106 | }
107 | private getHostAddress() {
108 | def parts = device.deviceNetworkId.split(":")
109 | log.debug device.deviceNetworkId
110 | def ip = convertHexToIP(parts[0])
111 | def port = convertHexToInt(parts[1])
112 | return ip + ":" + port
113 | }
114 |
--------------------------------------------------------------------------------
/smartapps/set tstat to not away.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Thermostat Away to Home
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Thermostat Away Mode Reset on Motion and Schedule",
18 | namespace: "ps",
19 | author: "patrick@patrickstuart.com",
20 | description: "Set the Nest thermostat (or any tstat) to home when motion is sensed or door is opened, etc.",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
24 |
25 |
26 | preferences {
27 |
28 | section("Thermostats") {
29 | input "tstat1", "capability.thermostat", title: "Which Tstats?", multiple: true
30 | }
31 |
32 | section("When there's movement..."){
33 | input "motion1", "capability.motionSensor", title: "Where?", multiple: true, required: false
34 | }
35 |
36 | section("Schedule") {
37 | input(name: "days", type: "enum", title: "Allow Automatic Away/Not Away On These Days", description: "",
38 | required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
39 |
40 | input( name: "timeAway", title: "Turn On Away Time?", type: "time", required: false)
41 |
42 | input( name: "timeHome", title: "Turn Off Away Time?", type: "time", required: false)
43 | }
44 |
45 |
46 |
47 | }
48 |
49 | def installed() {
50 | log.debug "Installed with settings: ${settings}"
51 | initialize()
52 | }
53 |
54 | def updated() {
55 | log.debug "Updated with settings: ${settings}"
56 | unsubscribe()
57 | initialize()
58 | }
59 |
60 | def initialize() {
61 | subscribe(motion1, "motion.active", motionHandler)
62 | subscribe(location, modeChangeHandler)
63 | unschedule(changeModeHome)
64 | unschedule(changeModeAway)
65 |
66 | schedule(timeHome, changeModeHome)
67 | schedule(timeAway, changeModeAway)
68 |
69 | }
70 |
71 | def modeChangeHandler(evt) {
72 | log.debug "event at mode change event is $evt"
73 | if (evt.value == "Home") {
74 | log.debug "Mode changed to Home"
75 | setHome()
76 | }
77 | if (evt.value == "Away") {
78 | log.debug "Mode changed to Away"
79 | setAway()
80 | }
81 | }
82 |
83 | def changeModeHome(evt) {
84 | log.debug "change Mode Home Fired"
85 | def today = new Date().format("EEEE")
86 | //log.debug "today: ${today}, days: ${days}"
87 |
88 | if (!days || days.contains(today)) {
89 | log.debug "Set to Not Away on $today"
90 | setHome()
91 | }
92 | }
93 |
94 | def changeModeAway(evt) {
95 | log.debug "change Mode Away Fired"
96 | def today = new Date().format("EEEE")
97 | if (!days || days.contains(today)) {
98 | log.debug "Set to Away on $today"
99 | setAway()
100 | }
101 | }
102 |
103 | def motionHandler(evt) {
104 | log.debug "Motion detected, $evt Set to Not Away"
105 | setHome()
106 | }
107 |
108 | def setHome() {
109 | for (t in settings.tstat1) {
110 | if (tstat1[0].latestValue("presence") == "away") {
111 | log.debug "Setting tstat to here"
112 | t.present()
113 | }
114 | }
115 | }
116 |
117 | def setAway() {
118 | for (t in settings.tstat1) {
119 | if (tstat1[0].latestValue("presence") == "present") {
120 | log.debug "Setting tstat to away"
121 | t.away()
122 | }
123 | }
124 | }
125 |
126 |
127 |
--------------------------------------------------------------------------------
/smartapps/dawn to dust motion dimmer:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_Dust to Dawn Motion Lights
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "ps_Dust to Dawn Motion Lights",
18 | namespace: "ps",
19 | author: "patrick@patrickstuart.com",
20 | description: "Turn lights on at Dust, motion bright, fade out no motion, turn off lights at Dawn...",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
24 |
25 |
26 | preferences {
27 | section("Select Dimmers you want to Use") {
28 | input "motions", "capability.motionSensor", title: "Motions", required: false, multiple: true
29 | input "switches", "capability.switchLevel", title: "Switches", required: false, multiple: true
30 | }
31 | section ("Zip code (optional, defaults to location coordinates)...") {
32 | input "zipCode", "text", required: false
33 | }
34 | }
35 |
36 | def installed() {
37 | log.debug "Installed with settings: ${settings}"
38 | initialize()
39 | }
40 |
41 | def updated() {
42 | log.debug "Updated with settings: ${settings}"
43 | unsubscribe()
44 | initialize()
45 | }
46 |
47 | def initialize() {
48 | subscribe(motions, "motion", handleMotionEvent)
49 | checkSun()
50 | }
51 |
52 | def checkSun() {
53 | def zip = settings.zip as String
54 | def sunInfo = getSunriseAndSunset(zipCode: zip)
55 | def current = now()
56 | if(sunInfo.sunrise.time > current || sunInfo.sunset.time < current) {
57 | state.sunMode = "SunsetMode"
58 | }
59 | else {
60 | state.sunMode = "SunriseMode"
61 | }
62 | log.info("Sunset: ${sunInfo.sunset.time}")
63 | log.info("Sunrise: ${sunInfo.sunrise.time}")
64 | log.info("Current: ${current}")
65 | log.info("sunMode: ${state.sunMode}")
66 |
67 | if(current < sunInfo.sunrise.time) {
68 | runIn(((sunInfo.sunrise.time - current) / 1000).toInteger(), setSunrise)
69 | }
70 |
71 | if(current < sunInfo.sunset.time) {
72 | runIn(((sunInfo.sunset.time - current) / 1000).toInteger(), setSunset)
73 | }
74 | schedule(timeTodayAfter(new Date(), "01:00", location.timeZone), checkSun)
75 | if (state.sunMode == "SunriseMode")
76 | {
77 | setSunrise()
78 | }
79 | else
80 | {
81 | setSunset()
82 | }
83 | }
84 |
85 | def handleMotionEvent(evt) {
86 | log.debug "Motion event Detected do something $evt"
87 | log.debug state
88 | state.motion = evt.value
89 |
90 | //testing force state.sunMode to SunsetMode
91 | //state.sunMode = "SunsetMode"
92 |
93 | if (state.sunMode == "SunsetMode" && state.motion == "active") {
94 | switches?.setLevel(100)
95 | state.Level = "99"
96 | log.debug "Set the switches to level 100"
97 | log.debug state
98 |
99 | //Change this to # of seconds to leave light on
100 | runIn(300, setSunset)
101 | }
102 | else
103 | {
104 | log.debug "it is after sunrise but before sunset, do nothing"
105 | //runIn(120, sunriseHandler)
106 | }
107 | }
108 |
109 | def setSunset() {
110 | log.debug "Sunset handler"
111 | switches?.setLevel(20)
112 | state.Level = "20"
113 | // if state.motion is false
114 | }
115 |
116 | def setSunrise() {
117 | log.info "Executing sunrise handler"
118 | switches?.setLevel(0)
119 | state.Level = "off"
120 | }
121 |
--------------------------------------------------------------------------------
/devicetypes/pstuart/insteon-moisture-local.src/insteon-moisture-local.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Insteon Moisture (LOCAL)
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | * Updated 1/4/15 by goldmichael@gmail.com
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 | * in compliance with the License. You may obtain a copy of the License at:
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
14 | * for the specific language governing permissions and limitations under the License.
15 | *
16 | */
17 | metadata {
18 | definition (name: "Insteon Moisture (LOCAL)", namespace: "pstuart", author: "patrick@patrickstuart.com") {
19 | capability "Switch"
20 | capability "Sensor"
21 | capability "Actuator"
22 | capability "Water Sensor"
23 | capability "Polling"
24 |
25 | command "status"
26 | }
27 |
28 | preferences {
29 | input("InsteonIP", "string", title:"Insteon IP Address", description: "Please enter your Insteon Hub IP Address", defaultValue: "192.168.1.2", required: true, displayDuringSetup: true)
30 | input("InsteonPort", "string", title:"Insteon Port", description: "Please enter your Insteon Hub Port", defaultValue: 25105, required: true, displayDuringSetup: true)
31 | input("InsteonID", "string", title:"Device Insteon ID", description: "Please enter the devices Insteon ID - numbers only", defaultValue: "1E65F2", required: true, displayDuringSetup: true)
32 | input("InsteonHubUsername", "string", title:"Insteon Hub Username", description: "Please enter your Insteon Hub Username", defaultValue: "michael" , required: true, displayDuringSetup: true)
33 | input("InsteonHubPassword", "string", title:"Insteon Hub Password", description: "Please enter your Insteon Hub Password", defaultValue: "password" , required: true, displayDuringSetup: true)
34 | }
35 |
36 | simulator {
37 |
38 | }
39 |
40 | // UI tile definitions
41 | tiles {
42 | standardTile("water", "device.water", width: 2, height: 2, canChangeIcon: false) {
43 | state "dry", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
44 | state "wet", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
45 | }
46 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
47 | state "battery", label:'${currentValue} battery', unit:""
48 | }
49 | standardTile("refresh", "device.button", inactiveLabel: false, decoration: "flat") {
50 | state "refresh", action:"status", icon:"st.secondary.refresh"
51 | }
52 |
53 | main "water"
54 | details(["water", "battery", "refresh"])
55 | }
56 | }
57 |
58 | def poll() {
59 | status()
60 | }
61 |
62 | def status() {
63 | log.debug "status hit"
64 | def host = InsteonIP
65 |
66 | def path = "/3?0262" + "${InsteonID}" + "0F19FF=I=3"
67 | log.debug "path is: $path"
68 |
69 |
70 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}"
71 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
72 | def headers = [:] //"HOST:"
73 | headers.put("HOST", "$host:$InsteonPort")
74 | headers.put("Authorization", userpass)
75 |
76 | try {
77 | def hubAction = new physicalgraph.device.HubAction(
78 | method: method,
79 | path: path,
80 | headers: headers
81 | )
82 | }
83 | catch (Exception e) {
84 | log.debug "Hit Exception on $hubAction"
85 | log.debug e
86 | }
87 |
88 | }
89 |
90 | def parse(String description) {
91 | log.debug "Parsing $description"
92 | def map = stringToMap(description)
93 |
94 | if (description == "0")
95 | {
96 | sendEvent(name: "water", value: "wet")
97 | }
98 | if (description == "1")
99 | {
100 | sendEvent(name: "water", value: "dry")
101 | }
102 | if (description == "2")
103 | {
104 | sendEvent(name: "battery", value: "low")
105 | }
106 | if (description == "3")
107 | {
108 | sendEvent(name: "battery", value: "ok")
109 | }
110 | }
--------------------------------------------------------------------------------
/smartapps/pstuart/live-code-hvac-fan-run-away.src/live-code-hvac-fan-run-away.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Live Code HVAC Fan Run Away
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Live Code HVAC Fan Run Away",
18 | namespace: "pstuart",
19 | author: "Patrick Stuart",
20 | description: "My wife's first one today smartapp request.",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
25 | oauth: true)
26 |
27 |
28 | preferences {
29 | section("Thermostats") {
30 | input "tstats", "capability.thermostat", title: "HVAC System", required:true, multiple: true
31 |
32 | }
33 |
34 | section("Set how long (in minutes) you want the fan to run, minimum of 5 minutes.") {
35 | input "fanmins", "number", title: "Minutes to Run Fan"
36 | }
37 | section ([mobileOnly:true]) {
38 | label title: "Assign a name", required: false
39 | mode title: "Set for specific mode(s)", required: false
40 | }
41 | }
42 |
43 | mappings {
44 | path("/html") {action: [GET: "html"]}
45 | path("/updates") {action: [GET: "updates"]}
46 | path("/:id/:command") {action: [GET: "deviceAction"]}
47 | }
48 |
49 | def installed() {
50 | log.debug "Installed with settings: ${settings}"
51 |
52 | initialize()
53 | }
54 |
55 | def updated() {
56 | log.debug "Updated with settings: ${settings}"
57 |
58 | unsubscribe()
59 | initialize()
60 | }
61 |
62 | def initialize() {
63 | // start the hvac fan
64 | // start a timer
65 | // at end of timer, stop hvac fan
66 | subscribe(app, startFan)
67 | startFan()
68 | log.debug "Fan Started"
69 | }
70 |
71 | def startFan(evt) {
72 | log.debug "startFan function hit"
73 | tstats*.fanOn()
74 | runIn(fanmins * 60, stopFan)
75 | }
76 |
77 | def stopFan(evt) {
78 | log.debug "stopFan function hit"
79 | unschedule()
80 | tstats*.fanAuto()
81 | log.debug "fan stopped"
82 |
83 | }
84 |
85 | def html() {
86 | state.updates = []
87 | render contentType: "text/html", data: """
88 |
89 |
90 | ${js()}
91 |
92 |
93 |
94 |
97 |
98 |
99 | ${sws()}
100 |
101 |
102 |
103 |
104 |
105 | """
106 | }
107 |
108 | def sws() {
109 | def markup = ""
110 | tstats.each {
111 | markup = markup + """
112 |
113 |
${it.displayName}
114 |
${it.id}
115 |
${it.currentValue("thermostatFanMode")}
116 |
117 |
118 | """
119 | }
120 |
121 | markup
122 | }
123 |
124 |
125 | def js() { """
126 |
127 |
133 | """
134 | }
135 |
136 | def deviceAction() {
137 | log.debug params.id
138 | log.debug params.command
139 | def sw = switches.find {it.id == params.id }
140 | if (sw.currentValue("thermostatFanMode") == "on") {
141 | sw.auto()
142 | render contentType: "text/html", data: """auto"""
143 | }
144 | else
145 | {
146 | sw.on()
147 | render contentType: "text/html", data: """on"""
148 | }
149 |
150 |
151 | }
--------------------------------------------------------------------------------
/smartapps/Control4 Send Command Test SmartApp.goovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Control4 Send Command Test
3 | *
4 | * Copyright 2014 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | definition(
17 | name: "Control4 Send Command Test",
18 | namespace: "",
19 | author: "Patrick Stuart",
20 | description: "Testing sending a http request to Control4 controller",
21 | category: "My Apps",
22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
24 | )
25 |
26 |
27 | preferences {
28 | section("When the door opens/closes...") {
29 | input "contact1", "capability.contactSensor"
30 | }
31 | section("When I touch the app, Send to server...") {
32 | input "server", "text", title: "Server IP", description: "Your HTTP Server IP", required: true
33 | input "command", "text", title: "Parameters", description: "Command to Send", required: true
34 | }
35 | }
36 |
37 | def installed() {
38 | log.debug "Installed with settings: ${settings}"
39 | subscribe(contact1.contact)
40 | subscribe(app, appTouch)
41 | initialize()
42 | }
43 |
44 | def updated() {
45 | log.debug "Updated with settings: ${settings}"
46 | unsubscribe()
47 | subscribe(contact1.contact)
48 | subscribe(app, appTouch)
49 | initialize()
50 | }
51 |
52 | def initialize() {
53 | subscribe(location, null, locationHandler, [filterEvents:false])
54 | // TODO: subscribe to attributes, devices, locations, etc.
55 | }
56 |
57 | // TODO: implement event handlers
58 |
59 |
60 | def appTouch(evt) {
61 | log.debug "appTouch: $evt"
62 | sendHttp()
63 | //sendSoap()
64 | }
65 |
66 | //def contact(evt)
67 | //{
68 | //def params = [
69 | // uri: 'https://192.168.101.165:8080/Test',
70 | // body: [param1: myParam1, param2: myParam2]
71 | // ]
72 |
73 | // log.debug "Params -> $params"
74 | // httpPost(params) {
75 | // response ->
76 | // log.debug "Response Received: Status [$response.status]"
77 | // }
78 | //}
79 |
80 | def sendSoap() {
81 |
82 | def ip = "${settings.server}:5020"
83 | def deviceNetworkId = "C0A865A5:139C"
84 | def cmdc4 = ''''''
85 | log.debug("c4 command:" + cmdc4)
86 | sendHubCommand(new physicalgraph.device.HubAction("""GET """ + cmdc4 + """HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
87 |
88 | log.debug("sent test command")
89 | log.debug location.hubs*.firmwareVersionString.findAll { it }
90 |
91 | }
92 |
93 |
94 |
95 |
96 |
97 | def sendHttp() {
98 | def ip = "${settings.server}:8080"
99 | def deviceNetworkId = "C0A865A5"
100 |
101 | sendHubCommand(new physicalgraph.device.HubAction("""GET /${settings.command} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
102 |
103 | log.debug("sent test command")
104 | log.debug location.hubs*.firmwareVersionString.findAll { it }
105 | }
106 |
107 |
108 | def locationHandler(evt) {
109 | def description = evt.description
110 | def hub = evt?.hubId
111 |
112 | log.debug "cp desc: " + description
113 | //log.debug description
114 | //def headerString = new String(description.decodeBase64())
115 |
116 | //log.debug("Count" + description.count(","))
117 | if (description.count(",") > 4)
118 | {
119 | def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64())
120 | log.debug(bodyString)
121 | }
122 |
123 |
124 | }
125 |
126 | private Long converIntToLong(ipAddress) {
127 | log.debug(ipAddress)
128 | long result = 0;;
129 | def parts = ipAddress.split("\\.")
130 | for (int i = 3; i >= 0; i--) {
131 | result |= (Long.parseLong(parts[3 - i]) << (i * 8));
132 | }
133 |
134 | return result & 0xFFFFFFFF;
135 | }
136 |
137 | private String convertIPToHex(ipAddress) {
138 | return Long.toHexString(converIntToLong(ipAddress));
139 | }
140 |
141 | private String getDeviceId() {
142 | def ip = convertIPToHex(settings.server)
143 | def port = Long.toHexString(Long.parseLong(settings.port))
144 | return ip + ":0x" + port
145 | }
146 |
--------------------------------------------------------------------------------
/devicetypes/GE Link Bulb.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_GE LinkBulb v2
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "ps_GE LinkBulb v2", namespace: "ps", author: "patrick@patrickstuart.com") {
18 |
19 | capability "Switch Level"
20 | capability "Actuator"
21 | capability "Switch"
22 | capability "Configuration"
23 | capability "Polling"
24 | capability "Refresh"
25 | capability "Sensor"
26 |
27 | fingerprint endpointId: "1", profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
28 | }
29 |
30 | // simulator metadata
31 | simulator {
32 | // status messages
33 | status "on": "on/off: 1"
34 | status "off": "on/off: 0"
35 |
36 | // reply messages
37 | reply "zcl on-off on": "on/off: 1"
38 | reply "zcl on-off off": "on/off: 0"
39 | }
40 |
41 | // UI tile definitions
42 | tiles {
43 | standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
44 | state "off", label: '${name}', action: "switch.on", icon: "st.Lighting.light13", backgroundColor: "#ffffff"
45 | state "on", label: '${name}', action: "switch.off", icon: "st.Lighting.light11", backgroundColor: "#79b821"
46 | }
47 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
48 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
49 | }
50 | controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
51 | state "color", action:"setAdjustedColor"
52 | }
53 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
54 | state "level", action:"switch level.setLevel"
55 | }
56 | valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
57 | state "level", label: 'Level ${currentValue}%'
58 | }
59 |
60 |
61 | main(["switch"])
62 | details(["switch", "levelSliderControl", "level", "refresh"])
63 | }
64 | }
65 |
66 | // Parse incoming device messages to generate events
67 | def parse(String description) {
68 | log.trace description
69 | if (description?.startsWith("catchall:")) {
70 | //def msg = zigbee.parse(description)
71 | //log.trace msg
72 | //log.trace "data: $msg.data"
73 | if(description?.endsWith("0100"))
74 | {
75 | def result = createEvent(name: "switch", value: "on")
76 | log.debug "Parse returned ${result?.descriptionText}"
77 | return result
78 | }
79 | if(description?.endsWith("0000"))
80 | {
81 | def result = createEvent(name: "switch", value: "off")
82 | log.debug "Parse returned ${result?.descriptionText}"
83 | return result
84 | }
85 | }
86 | if (description?.startsWith("read attr")) {
87 | log.debug description[-2..-1]
88 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
89 |
90 | sendEvent( name: "level", value: i )
91 | }
92 |
93 |
94 | }
95 |
96 | def on() {
97 | // just assume it works for now
98 | log.debug "on()"
99 | sendEvent(name: "switch", value: "on")
100 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
101 | }
102 |
103 | def off() {
104 | // just assume it works for now
105 | log.debug "off()"
106 | sendEvent(name: "switch", value: "off")
107 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
108 | }
109 |
110 | def refresh() {
111 | "st rattr 0x${device.deviceNetworkId} 1 6 0"
112 | "st rattr 0x${device.deviceNetworkId} 1 8 0"
113 | }
114 |
115 | def poll(){
116 | log.debug "Poll is calling refresh"
117 | refresh()
118 | }
119 |
120 | def setLevel(value) {
121 | log.trace "setLevel($value)"
122 | def cmds = []
123 |
124 | if (value == 0) {
125 | sendEvent(name: "switch", value: "off")
126 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
127 | }
128 | else if (device.latestValue("switch") == "off") {
129 | sendEvent(name: "switch", value: "on")
130 | }
131 |
132 | sendEvent(name: "level", value: value)
133 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16)
134 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
135 |
136 | //log.debug cmds
137 | cmds
138 | }
139 |
140 | private getEndpointId() {
141 | new BigInteger(device.endpointId, 16).toString()
142 | }
143 |
144 | private hex(value, width=2) {
145 | def s = new BigInteger(Math.round(value).toString()).toString(16)
146 | while (s.size() < width) {
147 | s = "0" + s
148 | }
149 | s
150 | }
151 |
152 | private Integer convertHexToInt(hex) {
153 | Integer.parseInt(hex,16)
154 | }
155 |
--------------------------------------------------------------------------------
/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Hue Bulb
3 | *
4 | * Author: SmartThings
5 | */
6 | // for the UI
7 | metadata {
8 | // Automatically generated. Make future change here.
9 | definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") {
10 | capability "Switch Level"
11 | capability "Actuator"
12 | capability "Color Control"
13 | capability "Switch"
14 | capability "Refresh"
15 | capability "Sensor"
16 |
17 | command "setAdjustedColor"
18 | command "reset"
19 | command "refresh"
20 | }
21 |
22 | simulator {
23 | // TODO: define status and reply messages here
24 | }
25 |
26 | tiles (scale: 2){
27 | multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
28 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
29 | attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
30 | attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
31 | attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
32 | attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
33 | }
34 | tileAttribute ("device.level", key: "SLIDER_CONTROL") {
35 | attributeState "level", action:"switch level.setLevel"
36 | }
37 | tileAttribute ("device.color", key: "COLOR_CONTROL") {
38 | attributeState "color", action:"setAdjustedColor"
39 | }
40 | }
41 |
42 | standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
43 | state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
44 | }
45 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
46 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
47 | }
48 | }
49 |
50 | main(["switch"])
51 | details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
52 | }
53 |
54 | // parse events into attributes
55 | def parse(description) {
56 | log.debug "parse() - $description"
57 | def results = []
58 | def map = description
59 | if (description instanceof String) {
60 | log.debug "Hue Bulb stringToMap - ${map}"
61 | map = stringToMap(description)
62 | }
63 | if (map?.name && map?.value) {
64 | results << createEvent(name: "${map?.name}", value: "${map?.value}")
65 | }
66 | results
67 | }
68 |
69 | // handle commands
70 | def on() {
71 | log.trace parent.on(this)
72 | sendEvent(name: "switch", value: "on")
73 | }
74 |
75 | def off() {
76 | log.trace parent.off(this)
77 | sendEvent(name: "switch", value: "off")
78 | }
79 |
80 | def nextLevel() {
81 | def level = device.latestValue("level") as Integer ?: 0
82 | if (level <= 100) {
83 | level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
84 | }
85 | else {
86 | level = 25
87 | }
88 | setLevel(level)
89 | }
90 |
91 | def setLevel(percent) {
92 | log.debug "Executing 'setLevel'"
93 | parent.setLevel(this, percent)
94 | sendEvent(name: "level", value: percent)
95 | }
96 |
97 | def setSaturation(percent) {
98 | log.debug "Executing 'setSaturation'"
99 | parent.setSaturation(this, percent)
100 | sendEvent(name: "saturation", value: percent)
101 | }
102 |
103 | def setHue(percent) {
104 | log.debug "Executing 'setHue'"
105 | parent.setHue(this, percent)
106 | sendEvent(name: "hue", value: percent)
107 | }
108 |
109 | def setColor(value) {
110 | log.debug "setColor: ${value}, $this"
111 | parent.setColor(this, value)
112 | if (value.hue) { sendEvent(name: "hue", value: value.hue)}
113 | if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
114 | if (value.hex) { sendEvent(name: "color", value: value.hex)}
115 | if (value.level) { sendEvent(name: "level", value: value.level)}
116 | if (value.switch) { sendEvent(name: "switch", value: value.switch)}
117 | }
118 |
119 | def reset() {
120 | log.debug "Executing 'reset'"
121 | def value = [level:100, hex:"#90C638", saturation:56, hue:23]
122 | setAdjustedColor(value)
123 | parent.poll()
124 | }
125 |
126 | def setAdjustedColor(value) {
127 | if (value) {
128 | log.trace "setAdjustedColor: ${value}"
129 | def adjusted = value + [:]
130 | adjusted.hue = adjustOutgoingHue(value.hue)
131 | // Needed because color picker always sends 100
132 | adjusted.level = null
133 | setColor(adjusted)
134 | }
135 | }
136 |
137 | def refresh() {
138 | log.debug "Executing 'refresh'"
139 | parent.manualRefresh()
140 | }
141 |
142 | def adjustOutgoingHue(percent) {
143 | def adjusted = percent
144 | if (percent > 31) {
145 | if (percent < 63.0) {
146 | adjusted = percent + (7 * (percent -30 ) / 32)
147 | }
148 | else if (percent < 73.0) {
149 | adjusted = 69 + (5 * (percent - 62) / 10)
150 | }
151 | else {
152 | adjusted = percent + (2 * (100 - percent) / 28)
153 | }
154 | }
155 | log.info "percent: $percent, adjusted: $adjusted"
156 | adjusted
157 | }
158 |
--------------------------------------------------------------------------------
/devicetypes/Test Temp Slider.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_TestTempSlider
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "ps_TestTempSlider", namespace: "ps", author: "patrick@patrickstuart.com") {
18 | capability "Polling"
19 | capability "Thermostat"
20 | capability "Temperature Measurement"
21 | }
22 |
23 |
24 | preferences {
25 | input("SliderMin", "Double", title: "Slider Min Adjust", description: "Set Slider Min")
26 | input("SliderMax", "Double", title: "Slider Max Adjust", description: "Set Slider Max")
27 | }
28 |
29 |
30 | simulator {
31 | // TODO: define status and reply messages here
32 |
33 | }
34 |
35 | tiles {
36 | // TODO: define your main and details tiles here
37 | valueTile("temperature", "device.temperature", width: 2, height: 2) {
38 | state("temperature", label:'${currentValue}°', unit:"F",
39 | backgroundColors:[
40 | [value: 31, color: "#153591"],
41 | [value: 44, color: "#1e9cbb"],
42 | [value: 59, color: "#90d2a7"],
43 | [value: 74, color: "#44b621"],
44 | [value: 84, color: "#f1d801"],
45 | [value: 95, color: "#d04e00"],
46 | [value: 96, color: "#bc2323"]
47 | ]
48 | )
49 | }
50 | valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
51 | state "thermostatSetpoint", label:'${currentValue}'
52 | }
53 | controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
54 | state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
55 | }
56 | standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
57 | state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
58 | }
59 | main "temperature"
60 | details(["temperature", "thermostatSetpoint","coolSliderControl", "refresh"])
61 |
62 | }
63 | }
64 |
65 | // parse events into attributes
66 | def parse(String description) {
67 | log.debug "Parsing '${description}'"
68 | // TODO: handle 'temperature' attribute
69 | // TODO: handle 'heatingSetpoint' attribute
70 | // TODO: handle 'coolingSetpoint' attribute
71 | // TODO: handle 'thermostatSetpoint' attribute
72 | // TODO: handle 'thermostatMode' attribute
73 | // TODO: handle 'thermostatFanMode' attribute
74 | // TODO: handle 'thermostatOperatingState' attribute
75 | // TODO: handle 'temperature' attribute
76 |
77 | }
78 |
79 | // handle commands
80 | def poll() {
81 | log.debug "Executing 'poll'"
82 | // TODO: handle 'poll' command
83 | }
84 |
85 | def setHeatingSetpoint() {
86 | log.debug "Executing 'setHeatingSetpoint'"
87 | // TODO: handle 'setHeatingSetpoint' command
88 | }
89 |
90 | def setCoolingSetpoint(tempF) {
91 | log.debug "Executing 'setCoolingSetpoint'"
92 | // TODO: handle 'setCoolingSetpoint' command
93 | log.debug tempF
94 | def privMin = 40
95 | def privMax = 90
96 | def PrefSet = false
97 | if (is(SliderMin))
98 | {
99 | PrefSet = true
100 | }
101 | if (is(SliderMax))
102 | {
103 | PrefSet = true
104 | }
105 |
106 | // So range is 1-100 need to map to a new min and max formula is y = ((x - a1)/(a2 - a1)) * (b2 - b1) + b1
107 | //OldRange = (OldMax - OldMin)
108 | //NewRange = (NewMax - NewMin)
109 | //NewValue = (((OldValue - OldMin) * NewRange) / OldRange) + NewMin
110 | //def newtempF = ((tempF.toInteger() - 1)/(100 - 1) * (privMax.toInteger() - privMin.toInteger()) + privMax.toInteger())
111 | def newtempF = (((tempF.toInteger() - 1) * (privMax.toInteger() - privMin.toInteger()) / (100/1) + privMin.toInteger()))
112 | log.debug newtempF
113 | //Set min and max for range
114 |
115 | sendEvent("name":"thermostatSetpoint", "value":newtempF)
116 | }
117 |
118 | def off() {
119 | log.debug "Executing 'off'"
120 | // TODO: handle 'off' command
121 | }
122 |
123 | def heat() {
124 | log.debug "Executing 'heat'"
125 | // TODO: handle 'heat' command
126 | }
127 |
128 | def emergencyHeat() {
129 | log.debug "Executing 'emergencyHeat'"
130 | // TODO: handle 'emergencyHeat' command
131 | }
132 |
133 | def cool() {
134 | log.debug "Executing 'cool'"
135 | // TODO: handle 'cool' command
136 | }
137 |
138 | def setThermostatMode() {
139 | log.debug "Executing 'setThermostatMode'"
140 | // TODO: handle 'setThermostatMode' command
141 | }
142 |
143 | def fanOn() {
144 | log.debug "Executing 'fanOn'"
145 | // TODO: handle 'fanOn' command
146 | }
147 |
148 | def fanAuto() {
149 | log.debug "Executing 'fanAuto'"
150 | // TODO: handle 'fanAuto' command
151 | }
152 |
153 | def fanCirculate() {
154 | log.debug "Executing 'fanCirculate'"
155 | // TODO: handle 'fanCirculate' command
156 | }
157 |
158 | def setThermostatFanMode() {
159 | log.debug "Executing 'setThermostatFanMode'"
160 | // TODO: handle 'setThermostatFanMode' command
161 | }
162 |
163 | def auto() {
164 | log.debug "Executing 'auto'"
165 | // TODO: handle 'auto' command
166 | }
167 |
--------------------------------------------------------------------------------
/devicetypes/pstuart/insteon-switch-local.src/insteon-switch-local.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Insteon Switch (LOCAL)
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | * Updated 1/4/15 by goldmichael@gmail.com
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 | * in compliance with the License. You may obtain a copy of the License at:
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
14 | * for the specific language governing permissions and limitations under the License.
15 | *
16 | */
17 | metadata {
18 | definition (name: "Insteon Switch (LOCAL)", namespace: "pstuart", author: "patrick@patrickstuart.com") {
19 | capability "Switch"
20 | capability "Sensor"
21 | capability "Actuator"
22 | capability "Water Sensor"
23 |
24 | command "status"
25 | }
26 |
27 | preferences {
28 | input("InsteonIP", "string", title:"Insteon IP Address", description: "Please enter your Insteon Hub IP Address", defaultValue: "192.168.1.2", required: true, displayDuringSetup: true)
29 | input("InsteonPort", "string", title:"Insteon Port", description: "Please enter your Insteon Hub Port", defaultValue: 25105, required: true, displayDuringSetup: true)
30 | input("InsteonID", "string", title:"Device Insteon ID", description: "Please enter the devices Insteon ID - numbers only", defaultValue: "1E65F2", required: true, displayDuringSetup: true)
31 | input("InsteonHubUsername", "string", title:"Insteon Hub Username", description: "Please enter your Insteon Hub Username", defaultValue: "michael" , required: true, displayDuringSetup: true)
32 | input("InsteonHubPassword", "string", title:"Insteon Hub Password", description: "Please enter your Insteon Hub Password", defaultValue: "password" , required: true, displayDuringSetup: true)
33 | }
34 |
35 | simulator {
36 |
37 | }
38 |
39 | // UI tile definitions
40 | tiles {
41 | standardTile("water", "device.water", width: 2, height: 2, canChangeIcon: true) {
42 | state "dry", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
43 | state "wet", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
44 | }
45 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
46 | state "battery", label:'${currentValue} battery', unit:""
47 | }
48 | standardTile("refresh", "device.button", inactiveLabel: false, decoration: "flat") {
49 | state "refresh", action:"status", icon:"st.secondary.refresh"
50 | }
51 |
52 | main "water"
53 | details(["water", "battery", "refresh"])
54 | }
55 | }
56 |
57 | // handle commands
58 | def on() {
59 | //log.debug "Executing 'take'"
60 | sendEvent(name: "water", value: "wet")
61 | def host = InsteonIP
62 |
63 | def path = "/3?0262" + "${InsteonID}" + "0F12FF=I=3"
64 | log.debug "path is: $path"
65 |
66 |
67 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}"
68 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
69 | def headers = [:] //"HOST:"
70 | headers.put("HOST", "$host:$InsteonPort")
71 | headers.put("Authorization", userpass)
72 |
73 |
74 | try {
75 | def hubAction = new physicalgraph.device.HubAction(
76 | method: method,
77 | path: path,
78 | headers: headers
79 | )
80 | }
81 | catch (Exception e) {
82 | log.debug "Hit Exception on $hubAction"
83 | log.debug e
84 | }
85 | }
86 |
87 | def status() {
88 | log.debug "status hit"
89 | def host = InsteonIP
90 |
91 | def path = "/3?0262" + "${InsteonID}" + "0F19FF=I=3"
92 | log.debug "path is: $path"
93 |
94 |
95 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}"
96 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
97 | def headers = [:] //"HOST:"
98 | headers.put("HOST", "$host:$InsteonPort")
99 | headers.put("Authorization", userpass)
100 |
101 | try {
102 | def hubAction = new physicalgraph.device.HubAction(
103 | method: method,
104 | path: path,
105 | headers: headers
106 | )
107 | }
108 | catch (Exception e) {
109 | log.debug "Hit Exception on $hubAction"
110 | log.debug e
111 | }
112 |
113 | }
114 |
115 | def off() {
116 | //log.debug "Executing 'take'"
117 | sendEvent(name: "water", value: "dry")
118 | def host = InsteonIP
119 |
120 | def path = "/3?0262" + "${InsteonID}" + "0F14FF=I=3"
121 | log.debug "path is: $path"
122 |
123 |
124 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}"
125 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
126 | def headers = [:] //"HOST:"
127 | headers.put("HOST", "$host:$InsteonPort")
128 | headers.put("Authorization", userpass)
129 |
130 | try {
131 | def hubAction = new physicalgraph.device.HubAction(
132 | method: method,
133 | path: path,
134 | headers: headers
135 | )
136 | }
137 | catch (Exception e) {
138 | log.debug "Hit Exception on $hubAction"
139 | log.debug e
140 | }
141 | }
142 |
143 | def parse(String description) {
144 | log.debug "Parsing $description"
145 | def map = stringToMap(description)
146 |
147 | if (description == "0")
148 | {
149 | sendEvent(name: "water", value: "wet")
150 | }
151 | if (description == "1")
152 | {
153 | sendEvent(name: "water", value: "dry")
154 | }
155 | if (description == "2")
156 | {
157 | sendEvent(name: "battery", value: "low")
158 | }
159 | if (description == "3")
160 | {
161 | sendEvent(name: "battery", value: "ok")
162 | }
163 | }
--------------------------------------------------------------------------------
/smartapps/sample-webservice.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample Web Services Application
3 | *
4 | * Author: SmartThings
5 | */
6 |
7 | preferences {
8 | section("Allow a web application to control these things...") {
9 | input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
10 | input "motions", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false
11 | input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
12 | }
13 | }
14 |
15 | mappings {
16 | path("/list") {
17 | action: [
18 | GET: "listAll"
19 | ]
20 | }
21 |
22 | path("/events/:id") {
23 | action: [
24 | GET: "showEvents"
25 | ]
26 | }
27 |
28 | path("/switches") {
29 | action: [
30 | GET: "listSwitches",
31 | PUT: "updateSwitches"
32 | ]
33 | }
34 | path("/switches/:id") {
35 | action: [
36 | GET: "showSwitch",
37 | PUT: "updateSwitch"
38 | ]
39 | }
40 | path("/switches/subscriptions") {
41 | action: [
42 | POST: "addSwitchSubscription"
43 | ]
44 | }
45 | path("/switches/subscriptions/:id") {
46 | action: [
47 | DELETE: "removeSwitchSubscription"
48 | ]
49 | }
50 |
51 | path("/motionSensors") {
52 | action: [
53 | GET: "listMotions",
54 | PUT: "updateMotions"
55 | ]
56 | }
57 | path("/motionSensors/:id") {
58 | action: [
59 | GET: "showMotion",
60 | PUT: "updateMotion"
61 | ]
62 | }
63 | path("/motionSensors/subscriptions") {
64 | action: [
65 | POST: "addMotionSubscription"
66 | ]
67 | }
68 | path("/motionSensors/subscriptions/:id") {
69 | action: [
70 | DELETE: "removeMotionSubscription"
71 | ]
72 | }
73 |
74 | path("/locks") {
75 | action: [
76 | GET: "listLocks",
77 | PUT: "updateLock"
78 | ]
79 | }
80 | path("/locks/:id") {
81 | action: [
82 | GET: "showLock",
83 | PUT: "updateLock"
84 | ]
85 | }
86 | path("/locks/subscriptions") {
87 | action: [
88 | POST: "addLockSubscription"
89 | ]
90 | }
91 | path("/locks/subscriptions/:id") {
92 | action: [
93 | DELETE: "removeLockSubscription"
94 | ]
95 | }
96 |
97 | path("/state") {
98 | action: [
99 | GET: "currentState"
100 | ]
101 | }
102 |
103 | }
104 |
105 | def installed() {log.trace "Installed"}
106 |
107 | def updated() {log.trace "Updated"}
108 |
109 | def listAll() {
110 | listSwitches() + listMotions() + listLocks()
111 | }
112 |
113 |
114 | def listSwitches() {
115 | switches.collect{device(it,"switch")}
116 | }
117 | void updateSwitches() {
118 | updateAll(switches)
119 | }
120 | def showSwitch() {
121 | show(switches, "switch")
122 | }
123 | void updateSwitch() {
124 | update(switches)
125 | }
126 | def addSwitchSubscription() {
127 | addSubscription(switches, "switch")
128 | }
129 | def removeSwitchSubscription() {
130 | removeSubscription(switches)
131 | }
132 |
133 | def listMotions() {
134 | motions.collect{device(it,"motionSensor")}
135 | }
136 | void updateMotions() {
137 | updateAll(motions)
138 | }
139 | def showMotion() {
140 | show(motions, "motionSensor")
141 | }
142 | void updateMotion() {
143 | update(motions)
144 | }
145 | def addMotionSubscription() {
146 | addSubscription(motions, "motion")
147 | }
148 | def removeMotionSubscription() {
149 | removeSubscription(motions)
150 | }
151 |
152 | def listLocks() {
153 | locks.collect{device(it,"lock")}
154 | }
155 | void updateLocks() {
156 | updateAll(locks)
157 | }
158 | def showLock() {
159 | show(locks, "lock")
160 | }
161 | void updateLock() {
162 | update(locks)
163 | }
164 | def addLockSubscription() {
165 | addSubscription(locks, "lock")
166 | }
167 | def removeLockSubscription() {
168 | removeSubscription(locks)
169 | }
170 |
171 | def deviceHandler(evt) {
172 | def deviceInfo = state[evt.deviceId]
173 | if (deviceInfo) {
174 | httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [value: evt.value]]) {
175 | log.debug "Event data successfully posted"
176 | }
177 | } else {
178 | log.debug "No subscribed device found"
179 | }
180 | }
181 |
182 | def currentState() {
183 | state
184 | }
185 |
186 | def showStates() {
187 | def device = (switches + motions + locks).find { it.id == params.id }
188 | if (!device) {
189 | httpError(404, "Switch not found")
190 | }
191 | else {
192 | device.events(params)
193 | }
194 | }
195 |
196 | private void updateAll(devices) {
197 | def command = request.JSON?.command
198 | if (command) {
199 | devices."$command"()
200 | }
201 | }
202 |
203 | private void update(devices) {
204 | log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
205 | def command = request.JSON?.command
206 | if (command) {
207 | def device = devices.find { it.id == params.id }
208 | if (!device) {
209 | httpError(404, "Device not found")
210 | } else {
211 | device."$command"()
212 | }
213 | }
214 | }
215 |
216 | private show(devices, type) {
217 | def device = devices.find { it.id == params.id }
218 | if (!device) {
219 | httpError(404, "Device not found")
220 | }
221 | else {
222 | def attributeName = type == "motionSensor" ? "motion" : type
223 | def s = device.currentState(attributeName)
224 | [id: device.id, label: device.displayName, value: s?.value, unitTime: s?.date?.time, type: type]
225 | }
226 | }
227 |
228 | private addSubscription(devices, attribute) {
229 | def deviceId = request.JSON?.deviceId
230 | def callbackUrl = request.JSON?.callbackUrl
231 | def myDevice = devices.find { it.id == deviceId }
232 | if (myDevice) {
233 | if (state[deviceId]) {
234 | log.debug "Switch subscription already exists, unsubcribing"
235 | unsubscribe(myDevice)
236 | }
237 | log.debug "Adding switch subscription" + callbackUrl
238 | state[deviceId] = [callbackUrl: callbackUrl]
239 | log.debug "Added state: $state"
240 | subscribe(myDevice, "switch", deviceHandler)
241 | }
242 | }
243 |
244 | private removeSubscription(devices) {
245 | def deviceId = params.id
246 | def device = devices.find { it.id == deviceId }
247 | if (device) {
248 | log.debug "Removing $device.displayName subscription"
249 | state.remove(device.id)
250 | unsubscribe(device)
251 | }
252 | }
253 |
254 | private device(it, type) {
255 | it ? [id: it.id, label: it.displayName, type: type] : null
256 | }
257 |
--------------------------------------------------------------------------------
/devicetypes/Get Ubi Sensors:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_Ubi
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 |
17 | preferences {
18 | input("username", "text", title: "Username", description: "Your Ubi Portal username (usually an email address)")
19 | input("password", "password", title: "Password", description: "Your Ubi password")
20 | }
21 |
22 | metadata {
23 | definition (name: "ps_Ubi", namespace: "ps", author: "patrick@patrickstuart.com") {
24 | capability "Polling"
25 | capability "Relative Humidity Measurement"
26 | capability "Illuminance Measurement"
27 | capability "Temperature Measurement"
28 |
29 |
30 | attribute "airPressure", "string"
31 | attribute "soundLevel", "string"
32 | }
33 |
34 | simulator {
35 |
36 | }
37 |
38 | tiles {
39 | valueTile("temperature", "device.temperature", width: 1, height: 1, canChangeIcon: false) {
40 | state("temperature", label: '${currentValue}°', unit:"F", backgroundColors: [
41 | [value: 31, color: "#153591"],
42 | [value: 44, color: "#1e9cbb"],
43 | [value: 59, color: "#90d2a7"],
44 | [value: 74, color: "#44b621"],
45 | [value: 84, color: "#f1d801"],
46 | [value: 95, color: "#d04e00"],
47 | [value: 96, color: "#bc2323"]
48 | ]
49 | )
50 | }
51 | valueTile("humidity", "device.humidity", inactiveLabel: false) {
52 | state "default", label:'${currentValue}%', unit:"Humidity"
53 | }
54 | valueTile("illuminance", "device.illuminance", inactiveLabel: false) {
55 | state "luminosity", label:'${currentValue}lux', unit:"Light"
56 | }
57 | valueTile("airPressure", "device.airPressure", inactiveLabel: false) {
58 | state "default", label:'${currentValue}Kpa', unit:"Air Pressure"
59 | }
60 | valueTile("soundLevel", "device.soundLevel", inactiveLabel: false) {
61 | state "default", label:'${currentValue}db', unit:"Sound"
62 | }
63 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat") {
64 | state "default", action:"polling.poll", icon:"st.secondary.refresh"
65 | }
66 |
67 |
68 | main "temperature"
69 | details(["temperature", "humidity", "illuminance", "airPressure", "soundLevel", "refresh"])
70 | }
71 | }
72 |
73 | // parse events into attributes
74 | def parse(String description) {
75 | log.debug "Parsing '${description}'"
76 | // TODO: handle 'humidity' attribute
77 | // TODO: handle 'illuminance' attribute
78 | // TODO: handle 'temperature' attribute
79 | // TODO: handle 'carbonMonoxide' attribute
80 |
81 | }
82 |
83 | // handle commands
84 | def poll() {
85 | log.debug "Executing 'poll'"
86 |
87 | // TODO: handle 'poll' command
88 | login()
89 | }
90 |
91 |
92 |
93 |
94 | def login() {
95 | def params = [
96 | uri: 'https://portal.theubi.com/login.do',
97 | headers: [
98 | 'Content-Type': 'application/x-www-form-urlencoded',
99 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
100 | 'Accept-Encoding': 'sdch',
101 | 'Host': 'portal.theubi.com',
102 | 'DNT': '1',
103 | 'Origin': 'https://portal.theubi.com/login.jsp',
104 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36'
105 | ],
106 | body: [j_username: username, j_password: password]
107 | ]
108 |
109 | data.cookies = ''
110 |
111 | httpPost(params) { response ->
112 | log.debug "Request was successful, $response.status"
113 | //log.debug response.headers
114 | response.getHeaders('Set-Cookie').each {
115 | String cookie = it.value.split(';')[0]
116 | //log.debug "Adding cookie to collection: $cookie"
117 | data.cookies = data.cookies + cookie + ';'
118 | }
119 | //log.debug "Cookies: $data.cookies"
120 |
121 | }
122 |
123 |
124 | def params2 = [
125 | uri: "https://portal.theubi.com/room/",
126 | headers: [
127 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
128 | 'Accept-Encoding': 'plain',
129 | 'Cookie': data.cookies
130 | ],
131 | ]
132 |
133 | httpGet(params2) { response2 ->
134 | log.debug "Request was successful, $response2.status"
135 | def doc = response2.data
136 | def x = new StringWriter()
137 | doc[0].children[1].children[5].children[1].writeTo(x)
138 | def linesAsList = x.toString().minus(" ").split( /\n|\r|\n\r|\r\n/ ).collect { it.replace( "'", '' ) }
139 | //log.debug linesAsList
140 | def v = [:]
141 | v.temp = linesAsList[6].minus("document.write(toFahrenheit(").minus("));") //temp in C
142 | v.temp = Math.round((Double.parseDouble(v.temp) * 1.8 + 32)*100) /100 // Temp in F
143 | v.humidity = Double.parseDouble(linesAsList[11].minus("Humidity").minus("%")) // humidity
144 | v.airPressure = linesAsList[12].minus("Air Pressure").minus("KPa") //Air Pressure
145 | //v.airPressure = Double.parseDouble(v.airPressure) / 100// into bar
146 | v.soundLevel = Double.parseDouble(linesAsList[13].minus("Sound level").minus("dB")) //Sound Level
147 | v.lightLevel = Double.parseDouble(linesAsList[14].minus("Light level").minus("lux")) //Light Level
148 | log.debug v
149 |
150 | sendEvent(name: 'temperature', value: v.temp)
151 | sendEvent(name: 'humidity', value: v.humidity)
152 | sendEvent(name: 'illuminance', value: v.lightLevel, unit: "lux")
153 | sendEvent(name: 'airPressure', value: v.airPressure.toString())
154 | sendEvent(name: 'soundLevel', value: v.soundLevel)
155 |
156 | }
157 |
158 | }
159 |
--------------------------------------------------------------------------------
/smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 SmartThings
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Laundry Monitor
14 | *
15 | * Author: SmartThings
16 | *
17 | * Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.
18 | *
19 | * Date: 2013-02-21
20 | */
21 |
22 | definition(
23 | name: "Laundry Monitor",
24 | namespace: "smartthings",
25 | author: "SmartThings",
26 | description: "Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.",
27 | category: "Convenience",
28 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner.png",
29 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner%402x.png"
30 | )
31 |
32 | preferences {
33 | section("Tell me when this washer/dryer has stopped..."){
34 | input "sensor1", "capability.accelerationSensor"
35 | }
36 | section("Via this number (optional, sends push notification if not specified)"){
37 | input("recipients", "contact", title: "Send notifications to") {
38 | input "phone", "phone", title: "Phone Number", required: false
39 | }
40 | }
41 | section("And by turning on these lights (optional)") {
42 | input "switches", "capability.switch", required: false, multiple: true, title: "Which lights?"
43 | input "lightMode", "enum", options: ["Flash Lights", "Turn On Lights"], required: false, defaultValue: "Turn On Lights", title: "Action?"
44 | }
45 | section("Time thresholds (in minutes, optional)"){
46 | input "cycleTime", "decimal", title: "Minimum cycle time", required: false, defaultValue: 10
47 | input "fillTime", "decimal", title: "Time to fill tub", required: false, defaultValue: 5
48 | input "cycleCount", "decimal", title: "Number of cycles", required: false, defaultValue : 2
49 | }
50 | }
51 |
52 | def installed()
53 | {
54 | initialize()
55 | }
56 |
57 | def updated()
58 | {
59 | unsubscribe()
60 | log.debug "Monitor has been reset by user in app"
61 | initialize()
62 | }
63 |
64 | def initialize() {
65 | subscribe(sensor1, "acceleration.active", accelerationActiveHandler)
66 | subscribe(sensor1, "acceleration.inactive", accelerationInactiveHandler)
67 | }
68 |
69 | def accelerationActiveHandler(evt) {
70 | log.trace "vibration has been detected"
71 | if (!state.isRunning) {
72 | log.info "Arming detector"
73 | state.isRunning = true
74 | state.startedAt = now()
75 | state.cycleCount = 0
76 | }
77 | state.stoppedAt = null
78 | }
79 |
80 | def accelerationInactiveHandler(evt) {
81 | log.trace "no vibration, isRunning: $state.isRunning"
82 | if (state.isRunning) {
83 | log.debug "startedAt: ${state.startedAt}, stoppedAt: ${state.stoppedAt}"
84 | if (!state.stoppedAt) {
85 | state.stoppedAt = now()
86 | def delay = Math.floor(fillTime * 60).toInteger()
87 | runIn(delay, checkRunning, [overwrite: false])
88 | log.debug "Waiting $delay and then will check to see if running / vibration"
89 | }
90 | }
91 | }
92 |
93 | def checkRunning() {
94 | log.trace "checkRunning()"
95 | if (state.isRunning) {
96 | def fillTimeMsec = fillTime ? fillTime * 60000 : 300000
97 | def sensorStates = sensor1.statesSince("acceleration", new Date((now() - fillTimeMsec) as Long))
98 |
99 | if (!sensorStates.find{it.value == "active"}) {
100 | log.debug "Sensor is not active / vibrating"
101 | def cycleTimeMsec = cycleTime ? cycleTime * 60000 : 600000
102 | def duration = now() - state.startedAt
103 |
104 | // test to see if we can loop through cycleCount
105 | // loop through cycleCount
106 | // for each cycle, check if it is still running
107 | // if still running, reset and check again
108 | // else, move to next cycle
109 | // if end of cycleCount, end
110 | if (state.cycleCount.toInteger() < cycleCount.toInteger()) {
111 |
112 | state.cycleCount = state.cycleCount.toInteger() + 1
113 | //might have to delay between each cycle?
114 |
115 | log.debug "Cycle $state.cycleCount is running"
116 | } else
117 |
118 | {
119 |
120 | if (duration - fillTimeMsec > cycleTimeMsec) {
121 | log.debug "Sending notification"
122 |
123 | def msg = "${sensor1.displayName} is finished"
124 | log.info msg
125 |
126 | if (location.contactBookEnabled) {
127 | sendNotificationToContacts(msg, recipients)
128 | }
129 | else {
130 |
131 | if (phone) {
132 | sendSms phone, msg
133 | } else {
134 | sendPush msg
135 | }
136 |
137 | }
138 |
139 | if (switches) {
140 | if (lightMode?.equals("Turn On Lights")) {
141 | switches.on()
142 | } else {
143 | flashLights()
144 | }
145 | }
146 | } else {
147 | log.debug "Not sending notification because machine wasn't running long enough $duration versus $cycleTimeMsec msec"
148 | }
149 | state.isRunning = false
150 | log.info "Disarming detector"
151 | }
152 | }
153 | else {
154 | log.debug "skipping notification because vibration detected again"
155 | }
156 | }
157 |
158 | else {
159 | log.debug "machine no longer running"
160 | }
161 | }
162 |
163 | private flashLights() {
164 | def doFlash = true
165 | def onFor = onFor ?: 1000
166 | def offFor = offFor ?: 1000
167 | def numFlashes = numFlashes ?: 3
168 |
169 | log.debug "LAST ACTIVATED IS: ${state.lastActivated}"
170 | if (state.lastActivated) {
171 | def elapsed = now() - state.lastActivated
172 | def sequenceTime = (numFlashes + 1) * (onFor + offFor)
173 | doFlash = elapsed > sequenceTime
174 | log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}"
175 | }
176 |
177 | if (doFlash) {
178 | log.debug "FLASHING $numFlashes times"
179 | state.lastActivated = now()
180 | log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}"
181 | def initialActionOn = switches.collect{it.currentSwitch != "on"}
182 | def delay = 1L
183 | numFlashes.times {
184 | log.trace "Switch on after $delay msec"
185 | switches.eachWithIndex {s, i ->
186 | if (initialActionOn[i]) {
187 | s.on(delay: delay)
188 | }
189 | else {
190 | s.off(delay:delay)
191 | }
192 | }
193 | delay += onFor
194 | log.trace "Switch off after $delay msec"
195 | switches.eachWithIndex {s, i ->
196 | if (initialActionOn[i]) {
197 | s.off(delay: delay)
198 | }
199 | else {
200 | s.on(delay:delay)
201 | }
202 | }
203 | delay += offFor
204 | }
205 | }
206 | }
--------------------------------------------------------------------------------
/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * LIFX Color Bulb
3 | *
4 | * Copyright 2015 LIFX
5 | *
6 | */
7 | metadata {
8 | definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") {
9 | capability "Actuator"
10 | capability "Color Control"
11 | capability "Color Temperature"
12 | capability "Switch"
13 | capability "Switch Level" // brightness
14 | capability "Polling"
15 | capability "Refresh"
16 | capability "Sensor"
17 | }
18 |
19 | simulator {
20 | // TODO: define status and reply messages here
21 | }
22 |
23 | tiles {
24 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
25 | state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
26 | state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
27 | state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
28 | state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
29 | state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
30 | }
31 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
32 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
33 | }
34 | valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
35 | state "default", label:''
36 | }
37 |
38 | controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
39 | state "color", action:"setColor"
40 | }
41 |
42 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
43 | state "level", action:"switch level.setLevel"
44 | }
45 | valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
46 | state "level", label: '${currentValue}%'
47 | }
48 |
49 | controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
50 | state "colorTemp", action:"color temperature.setColorTemperature"
51 | }
52 | valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
53 | state "colorTemp", label: '${currentValue}K'
54 | }
55 |
56 | main(["switch"])
57 | details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
58 | }
59 | }
60 |
61 | // parse events into attributes
62 | def parse(String description) {
63 | if (description == 'updated') {
64 | return // don't poll when config settings is being updated as it may time out
65 | }
66 | poll()
67 | }
68 |
69 | // handle commands
70 | def setHue(percentage) {
71 | log.debug "setHue ${percentage}"
72 | parent.logErrors(logObject: log) {
73 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
74 | if (resp.status < 300) {
75 | sendEvent(name: "hue", value: percentage)
76 | sendEvent(name: "switch", value: "on")
77 | } else {
78 | log.error("Bad setHue result: [${resp.status}] ${resp.data}")
79 | }
80 | }
81 | }
82 |
83 | def setSaturation(percentage) {
84 | log.debug "setSaturation ${percentage}"
85 | parent.logErrors(logObject: log) {
86 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
87 | if (resp.status < 300) {
88 | sendEvent(name: "saturation", value: percentage)
89 | sendEvent(name: "switch", value: "on")
90 | } else {
91 | log.error("Bad setSaturation result: [${resp.status}] ${resp.data}")
92 | }
93 | }
94 | }
95 |
96 | def setColor(Map color) {
97 | log.debug "setColor ${color}"
98 | def attrs = []
99 | def events = []
100 | color.each { key, value ->
101 | switch (key) {
102 | case "hue":
103 | attrs << "hue:${value * 3.6}"
104 | events << createEvent(name: "hue", value: value)
105 | break
106 | case "saturation":
107 | attrs << "saturation:${value / 100}"
108 | events << createEvent(name: "saturation", value: value)
109 | break
110 | case "colorTemperature":
111 | attrs << "kelvin:${value}"
112 | events << createEvent(name: "colorTemperature", value: value)
113 | break
114 | }
115 | }
116 | parent.logErrors(logObject:log) {
117 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
118 | if (resp.status < 300) {
119 | sendEvent(name: "color", value: color.hex)
120 | sendEvent(name: "switch", value: "on")
121 | events.each { sendEvent(it) }
122 | } else {
123 | log.error("Bad setColor result: [${resp.status}] ${resp.data}")
124 | }
125 | }
126 | }
127 |
128 | def setLevel(percentage) {
129 | log.debug "setLevel ${percentage}"
130 | if (percentage < 1 && percentage > 0) {
131 | percentage = 1 // clamp to 1%
132 | }
133 | if (percentage == 0) {
134 | sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
135 | return off() // if the brightness is set to 0, just turn it off
136 | }
137 | parent.logErrors(logObject:log) {
138 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
139 | if (resp.status < 300) {
140 | sendEvent(name: "level", value: percentage)
141 | sendEvent(name: "switch", value: "on")
142 | } else {
143 | log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
144 | }
145 | }
146 | }
147 |
148 | def setColorTemperature(kelvin) {
149 | log.debug "Executing 'setColorTemperature' to ${kelvin}"
150 | parent.logErrors() {
151 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
152 | if (resp.status < 300) {
153 | sendEvent(name: "colorTemperature", value: kelvin)
154 | sendEvent(name: "color", value: "#ffffff")
155 | sendEvent(name: "saturation", value: 0)
156 | } else {
157 | log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
158 | }
159 |
160 | }
161 | }
162 |
163 | def on() {
164 | log.debug "Device setOn"
165 | parent.logErrors() {
166 | if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
167 | sendEvent(name: "switch", value: "on")
168 | }
169 | }
170 | }
171 |
172 | def off() {
173 | log.debug "Device setOff"
174 | parent.logErrors() {
175 | if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
176 | sendEvent(name: "switch", value: "off")
177 | }
178 | }
179 | }
180 |
181 | def poll() {
182 | log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
183 | def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
184 | if (resp.status != 200) {
185 | log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
186 | return []
187 | }
188 | def data = resp.data
189 |
190 | sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
191 | sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
192 | sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
193 | sendEvent(name: "hue", value: data.color.hue / 3.6)
194 | sendEvent(name: "saturation", value: data.color.saturation * 100)
195 | sendEvent(name: "colorTemperature", value: data.color.kelvin)
196 |
197 | return []
198 | }
199 |
200 | def refresh() {
201 | log.debug "Executing 'refresh'"
202 | poll()
203 | }
204 |
--------------------------------------------------------------------------------
/devicetypes/pstuart/generic-camera.src/generic-camera.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Generic Camera Device v1.1.01302017
4 | *
5 | * Copyright 2017 patrick@patrickstuart.com
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 | * in compliance with the License. You may obtain a copy of the License at:
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
14 | * for the specific language governing permissions and limitations under the License.
15 | *
16 | */
17 |
18 | metadata {
19 | definition (name: "Generic Camera", namespace: "pstuart", author: "patrick@patrickstuart.com") {
20 | capability "Image Capture"
21 | capability "Sensor"
22 | capability "Actuator"
23 |
24 | attribute "hubactionMode", "string"
25 |
26 | }
27 |
28 | preferences {
29 | input("CameraIP", "string", title:"Camera IP Address", description: "Please enter your camera's IP Address", required: true, displayDuringSetup: true)
30 | input("CameraPort", "string", title:"Camera Port", description: "Please enter your camera's Port", defaultValue: 80 , required: true, displayDuringSetup: true)
31 | input("CameraPath", "string", title:"Camera Path to Image", description: "Please enter the path to the image", defaultValue: "/SnapshotJPEG?Resolution=640x480&Quality=Clarity", required: true, displayDuringSetup: true)
32 | input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Please choose if the camera requires authentication (only basic is supported)", defaultValue: true, displayDuringSetup: true)
33 | input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Please choose if the camera uses a POST or a GET command to retreive the image", defaultValue: "GET", displayDuringSetup: true)
34 | input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true)
35 | input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true)
36 | }
37 |
38 | simulator {
39 |
40 | }
41 | /*
42 | tiles {
43 | standardTile("camera", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: true) {
44 | state "default", label: "", action: "", icon: "st.camera.dropcam-centered", backgroundColor: "#FFFFFF"
45 | }
46 |
47 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { }
48 |
49 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
50 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
51 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0"
52 | //state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
53 | }
54 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
55 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF"
56 | }
57 | main "camera"
58 | details(["cameraDetails", "blank", "take"])
59 | }
60 |
61 | */
62 | tiles {
63 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
64 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
65 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0"
66 | state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking"
67 | }
68 |
69 | standardTile("refresh", "device.alarmStatus", inactiveLabel: false, decoration: "flat") {
70 | state "refresh", action:"polling.poll", icon:"st.secondary.refresh"
71 | }
72 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
73 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF"
74 | }
75 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { }
76 | main "take"
77 | details([ "take", "blank", "refresh", "cameraDetails"])
78 | }
79 | }
80 |
81 | def parse(String description) {
82 | log.debug "Parsing '${description}'"
83 | def map = [:]
84 | def retResult = []
85 | def descMap = parseDescriptionAsMap(description)
86 | //Image
87 | def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"]
88 | if (imageKey) {
89 | storeTemporaryImage(imageKey, getPictureName())
90 | }
91 | }
92 |
93 | // handle commands
94 | def take() {
95 | def userpassascii = "${CameraUser}:${CameraPassword}"
96 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
97 | def host = CameraIP
98 | def hosthex = convertIPtoHex(host).toUpperCase() //thanks to @foxxyben for catching this
99 | def porthex = convertPortToHex(CameraPort).toUpperCase()
100 | device.deviceNetworkId = "$hosthex:$porthex"
101 |
102 | log.debug "The device id configured is: $device.deviceNetworkId"
103 |
104 | def path = CameraPath
105 | log.debug "path is: $path"
106 | log.debug "Requires Auth: $CameraAuth"
107 | log.debug "Uses which method: $CameraPostGet"
108 |
109 | def headers = [:]
110 | headers.put("HOST", "$host:$CameraPort")
111 | if (CameraAuth) {
112 | headers.put("Authorization", userpass)
113 | }
114 |
115 | log.debug "The Header is $headers"
116 |
117 | def method = "GET"
118 | try {
119 | if (CameraPostGet.toUpperCase() == "POST") {
120 | method = "POST"
121 | }
122 | }
123 | catch (Exception e) { // HACK to get around default values not setting in devices
124 | settings.CameraPostGet = "GET"
125 | log.debug e
126 | log.debug "You must not of set the perference for the CameraPOSTGET option"
127 | }
128 |
129 | log.debug "The method is $method"
130 |
131 | try {
132 | def hubAction = new physicalgraph.device.HubAction(
133 | method: method,
134 | path: path,
135 | headers: headers
136 | )
137 |
138 | hubAction.options = [outputMsgToS3:true]
139 | log.debug hubAction
140 | hubAction
141 | }
142 | catch (Exception e) {
143 | log.debug "Hit Exception $e on $hubAction"
144 | }
145 |
146 | }
147 |
148 | def parseDescriptionAsMap(description) {
149 | description.split(",").inject([:]) { map, param ->
150 | def nameAndValue = param.split(":")
151 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
152 | }
153 | }
154 |
155 | private getPictureName() {
156 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
157 | log.debug pictureUuid
158 | def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg"
159 | return picName
160 | }
161 |
162 | private String convertIPtoHex(ipAddress) {
163 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
164 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
165 | return hex
166 |
167 | }
168 |
169 | private String convertPortToHex(port) {
170 | String hexport = port.toString().format( '%04x', port.toInteger() )
171 | log.debug hexport
172 | return hexport
173 | }
174 |
175 | private Integer convertHexToInt(hex) {
176 | Integer.parseInt(hex,16)
177 | }
178 |
179 |
180 | private String convertHexToIP(hex) {
181 | log.debug("Convert hex to ip: $hex")
182 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
183 | }
184 |
185 | private getHostAddress() {
186 | def parts = device.deviceNetworkId.split(":")
187 | log.debug device.deviceNetworkId
188 | def ip = convertHexToIP(parts[0])
189 | def port = convertHexToInt(parts[1])
190 | return ip + ":" + port
191 | }
192 |
--------------------------------------------------------------------------------
/devicetypes/ps/ps-cardaccess-zigbee-to-ir.src/ps-cardaccess-zigbee-to-ir.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_CardAccess Zigbee to IR
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "ps_CardAccess Zigbee to IR", namespace: "ps", author: "patrick@patrickstuart.com") {
18 | capability "Contact Sensor"
19 | capability "Polling"
20 | capability "Signal Strength"
21 | capability "Sensor"
22 | capability "Refresh"
23 | capability "Configuration"
24 | capability "Button"
25 |
26 | command "test"
27 | command "getClusters"
28 |
29 | fingerprint endpointId: "01", deviceId: "0101", profileId: "0104", inClusters: "0000, 000A, 0099" //01 0104 0101 00 03 0000 000A 0099 00
30 | //fingerprint endpointId: "01", profileId: "0104", deviceId: "0101", inClusters: "0000 0003 0004 0005 0006 0008 000A"
31 | //fingerprint inClusters: "000A", endpointId: "01", deviceId: "0101", profileId: "0104", deviceVersion: "00"
32 | //fingerprint inClusters: "0408", outClusters: "AD0A", deviceVersion: "02", profileId: "C25D", endpointId: "91", deviceId: "0001"
33 | //fingerprint endpointId: "0C", profileId: "0104", deviceId: "0101", deviceVersion: "00", inClusters: "0000, 000A, 0099"
34 | //01 0104 0101 00 03 0000 000A 0099 00
35 | }
36 |
37 | simulator {
38 | // TODO: define status and reply messages here
39 | }
40 |
41 | tiles {
42 | standardTile("SendIR", "device.button") {
43 | state "SendIR", action:"test2", label:'${name}', icon:"st.unknown.zwave.remote-controller", backgroundColor: '#e14902'
44 | }
45 | standardTile("Test", "device.button") {
46 | state "Test", label:'${name}', action:"test", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902'
47 | }
48 | standardTile("Test2", "device.button") {
49 | state "Test2", label:'${name}', action:"test2", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902'
50 | }
51 | standardTile("refresh", "device.alarmStatus", inactiveLabel: false, decoration: "flat") {
52 | state "refresh", action:"refresh", icon:"st.secondary.refresh"
53 | }
54 |
55 | main(["SendIR"])
56 | details(["SendIR", "Test", "Test2","refresh"])
57 | }
58 | }
59 |
60 | // parse events into attributes
61 | def parse(String description) {
62 | log.debug "Parsing '${description}'"
63 | if (description?.startsWith("catchall:")) {
64 | def msg = zigbee.parse(description)
65 | log.trace msg
66 | //log.trace "data: $msg.data"
67 | //log.debug msg
68 | def payloadhex = description.tokenize(" ").last()
69 | def payload = payloadhex.decodeHex()
70 | def x = ""
71 |
72 | payload.each() { x += it as char }
73 |
74 | log.debug "Payload is $x"
75 |
76 |
77 | }
78 |
79 |
80 | }
81 |
82 |
83 | // handle commands
84 | def poll() {
85 | log.debug "Executing 'poll'"
86 | // TODO: handle 'poll' command
87 | }
88 |
89 | def refresh() {
90 | log.debug "Refresh hit"
91 | /*
92 | //zigbee.refreshData("0", "4") + zigbee.refreshData("0", "5")
93 |
94 | //def payload = "0s1234 c4.wc0 q01\r\n".encodeAsHex()
95 | def payload = "0s1234 c4.wc0 q01\r\n".encodeAsHex()
96 | log.debug payload
97 | log.debug swapEndianHex(payload)
98 | //"send 0x${device.deviceNetworkId} 1 C25C {${swapEndianHex(payload)}}"
99 | //"st cmd 0x${device.deviceNetworkId} ${endpointId} C25C 1 {${swapEndianHex(payload)}}"
100 | //def cmd = "send 0x${device.deviceNetworkId} C5 C5 {${swapEndianHex(payload)}}"
101 | def cmd = [
102 | //"zdo bind 0x${device.deviceNetworkId} 0C 01 0099 {${device.zigbeeId}} {}",
103 | //"zdo bind 0x${device.deviceNetworkId} 0x01 0x0c 0x0101 {${device.zigbeeId}} {}",
104 | //"delay 500",
105 | //"zcl global send-me-a-report 0104 0C 0099 0 67 {71}",
106 | //"delay 500",
107 | "st send 0x${device.deviceNetworkId} 0C 01 0C {6771}"
108 | ]
109 | //def cmd = "st cmd 0x${device.deviceNetworkId} 0x0C 0x0099 0x67 {0x71}"
110 | //def cmd = "zcl global send-me-a-report 0104 0C 0099 0 67 {71}"
111 | log.debug cmd
112 | cmd
113 | /*
114 | [
115 | "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 500",
116 | "st rattr 0x${device.deviceNetworkId} 1 0101 0", "delay 500",
117 | "st rattr 0x${device.deviceNetworkId} 1 C25C 0xC5", "delay 500",
118 | "st rattr 0x${device.deviceNetworkId} 1 C25C 0x0c40", "delay 500"
119 | ]
120 | */
121 |
122 | }
123 |
124 | def configure() {
125 | /*String zigbeeId = swapEndianHex(device.hub.zigbeeId)
126 | log.debug "Confuguring Reporting and Bindings."
127 | def configCmds = [
128 | "zdo bind 0x${device.deviceNetworkId} 0x01 0x0c 0x0101 {${device.zigbeeId}} {}",
129 | "delay 500",
130 | "zcl global send-me-a-report 0104 0C 0099 0 67 {71}",
131 | "delay 500"
132 | //Lock Reporting
133 | //"zcl global send-me-a-report 0104 0 0x30 0 3600 {01}", "delay 500",
134 | //"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
135 |
136 | //Battery Reporting
137 | //"zcl global send-me-a-report 1 0x20 0x20 5 3600 {}", "delay 200",
138 | //"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
139 |
140 |
141 | //"zdo bind 0x${device.deviceNetworkId} 1 1 0101 {${device.zigbeeId}} {}", "delay 500",
142 | //"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
143 |
144 | ]
145 | return configCmds + refresh()
146 | */
147 | }
148 |
149 | def test() {
150 | log.debug "test hit"
151 | //"st rattr 0x${device.deviceNetworkId} 0xc5 0c001 0"
152 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 67 {71}"
153 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 67 {73 6c 00 00 ff 10 10 80}" //turn led on to green for 120 sec
154 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 01 01}" //send ir command on channel 1
155 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 02 01}" //unknown
156 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 01 02}" //double ir duration
157 | //0x74 == 't' for transmit on port 1
158 | //0x04 == length of payload to follow
159 | //0x00 is 'code slot' of cached ir code
160 | //0x00 is the ir format (normal=0, no_carrier=1, inverted=2, direct=3)
161 | // last two bytes are repeat count with LSByte first "little-endian"
162 |
163 | "st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 06 00}"
164 | /*
165 | response of type 0x74 contains current status.
166 | Following byte (0xc4) is a status = bitmask of DELAYING=0x80, COMPLETED=0x08, TRANSMITTING=0x04, CONTINUOUS=0x02, IDLE=0x00
167 | So, the first response of 0x74, 0xC4... means transmitting, delaying (timed transmit)
168 | the second response of 0x74 0x08 means transmit COMPLETED
169 | The extra byte that follow the TRANSMITTING status are the CRC of the cached IR payload in the Z2IR.
170 | The driver can use this to verify that what the Z2IR is transmitting matches what the driver *thinks* is being transmitted.
171 |
172 | */
173 | }
174 |
175 | def test2() {
176 | log.debug "Hit test 2"
177 | "st rattr 0x${device.deviceNetworkId} 0xc5 0 0"
178 | }
179 |
180 | def getClusters() {
181 | log.debug "getClusters hit $device.deviceNetworkId"
182 | //"st rattr 0x${device.deviceNetworkId} 4 6 0x01"
183 | "zdo active 0x${device.deviceNetworkId}"
184 | }
185 |
186 | private hex(value, width=2) {
187 | def s = new BigInteger(Math.round(value).toString()).toString(16)
188 | while (s.size() < width) {
189 | s = "0" + s
190 | }
191 | s
192 | }
193 |
194 | private Integer convertHexToInt(hex) {
195 | Integer.parseInt(hex,16)
196 | }
197 |
198 | private String swapEndianHex(String hex) {
199 | reverseArray(hex.decodeHex()).encodeHex()
200 | }
201 |
202 | private byte[] reverseArray(byte[] array) {
203 | int i = 0;
204 | int j = array.length - 1;
205 | byte tmp;
206 | while (j > i) {
207 | tmp = array[j];
208 | array[j] = array[i];
209 | array[i] = tmp;
210 | j--;
211 | i++;
212 | }
213 | return array
214 | }
--------------------------------------------------------------------------------
/devicetypes/Control4 Zigbee HA Dimmer.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_Control4_Dimmer_ZigbeeHA
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | */
16 | metadata {
17 | definition (name: "ps_Control4_Dimmer_ZigbeeHA", namespace: "ps", author: "patrick@patrickstuart.com") {
18 | capability "Switch Level"
19 | capability "Actuator"
20 | capability "Switch"
21 | capability "Configuration"
22 | capability "Refresh"
23 | capability "Polling"
24 |
25 | fingerprint endpointId: "01", profileId: "0104", deviceId: "0101", inClusters: "0000 0003 0004 0005 0006 0008 000A"
26 | fingerprint endpointId: "C4", profileId: "C25D", deviceId: "0101", inClusters: "0001"
27 |
28 | }
29 |
30 | preferences {
31 | input("OnSpeed", "text", title:"Turn On Speed", description: "Please enter the speed at which the dimmer turns on", defaultValue:"1500", required: true, displayDuringSetup: true)
32 | input("OffSpeed", "text", title:"Turn Off Speed", description: "Please enter the speed at which the dimmer turns off", defaultValue:"1500", required: true, displayDuringSetup: true)
33 | input("DefaultOnValue", "text", title:"Default On Value", description: "Please enter the default value you want ST to turn on to, in case the last dimmed value is lost.", defaultValue:"75", required: true, displayDuringSetup: true)
34 |
35 | }
36 | simulator {
37 | // status messages
38 | status "on": "on/off: 1"
39 | status "off": "on/off: 0"
40 |
41 | // reply messages
42 | reply "zcl on-off on": "on/off: 1"
43 | reply "zcl on-off off": "on/off: 0"
44 |
45 | command "test"
46 | command "getClusters"
47 | }
48 |
49 | tiles {
50 | standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
51 | state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
52 | state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
53 | }
54 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
55 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
56 | }
57 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
58 | state "level", action:"switch level.setLevel"
59 | }
60 | valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
61 | state "level", label: 'Level ${currentValue}%'
62 | }
63 |
64 | main(["switch"])
65 | details(["switch", "levelSliderControl", "level", "refresh"])
66 | }
67 | }
68 |
69 | def parse(String description) {
70 | //log.trace description
71 | if (description?.startsWith("catchall: C25")) {
72 | def msg = zigbee.parse(description)
73 | //log.trace msg
74 | def payloadhex = description.tokenize(" ").last()
75 | def payload = payloadhex.decodeHex()
76 | def x = ""
77 | payload.each() { x += it as char }
78 |
79 | //log.debug "Payload is $x"
80 |
81 | if(x.contains("sa c4.dm.cc 00 01"))
82 | {
83 | def result = createEvent(name: "switch", value: "on")
84 | log.debug "Parse returned ${result?.descriptionText}"
85 | return result
86 | }
87 | if(x.contains("sa c4.dm.cc 00 00"))
88 | {
89 | def result = createEvent(name: "switch", value: "off")
90 | log.debug "Parse returned ${result?.descriptionText}"
91 | return result
92 | }
93 | if(x.contains("sa c4.dm.cc 00 02"))
94 | {
95 | log.debug "Double Tap Top"
96 | }
97 | if(x.contains("sa c4.dm.cc 01 02"))
98 | {
99 | log.debug "Double Tap Bottom"
100 | }
101 | if(x.contains("sa c4.dm.cc 00 03"))
102 | {
103 | log.debug "Triple Tap Top"
104 | }
105 | if(x.contains("sa c4.dm.cc 01 03"))
106 | {
107 | log.debug "Triple Tap Bottom"
108 | }
109 | if(x.contains("sa c4.dm.t0c") || x.contains("sa c4.dm.b0c")) {
110 | log.debug "switch is dimming $x"
111 | def l = convertHexToInt(x.tokenize(" ").last().split())
112 | log.debug l
113 | def i = Math.round(l)
114 | sendEvent( name: "level", value: i )
115 | }
116 |
117 | }
118 |
119 | if (description?.startsWith("read attr")) {
120 | //log.debug "Read Attr found"
121 | //def descMap = parseDescriptionAsMap(description)
122 | //log.debug descMap
123 | //log.debug description[-2..-1]
124 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
125 |
126 | sendEvent( name: "level", value: i )
127 | }
128 | }
129 |
130 | def test() {
131 | /*
132 | def cmd = []
133 | cmd << "zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}"
134 | cmd << "delay 500"
135 | cmd << "send 0x${device.deviceNetworkId} 1 6"
136 | cmd << "delay 1000"
137 | cmd
138 | */
139 | //'zcl on-off on'
140 | log.debug "$OnSpeed onspeed $OffSpeed offspeed $DefaultOnValue defaultOnValue $state.lastOnValue is state.lastonvalue"
141 | }
142 |
143 | def getClusters() {
144 | log.debug "getClusters hit $device.deviceNetworkId"
145 | //"st rattr 0x${device.deviceNetworkId} 4 6 0x01"
146 | "zdo active 0x${device.deviceNetworkId}"
147 | }
148 |
149 | def on() {
150 | log.debug "on()"
151 | sendEvent(name: "switch", value: "on")
152 |
153 | //"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
154 |
155 | //get level in UI
156 | def value = device.currentValue("level")
157 | if (value == 0) { value = state.lastOnValue
158 | log.debug "Value is $value"
159 | }
160 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16)
161 | //log.debug level
162 |
163 | def speed = OnSpeed //.toString().padLeft(4, '0')
164 | log.debug speed
165 | "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${speed} }"
166 | }
167 |
168 | def off() {
169 | log.debug "off()"
170 | //state.lastOnValue = device.currentValue("level")
171 | sendEvent(name: "switch", value: "off")
172 | //"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
173 | def speed = OffSpeed.toString().padLeft(4, '0')
174 | log.debug speed
175 | "st cmd 0x${device.deviceNetworkId} 1 8 4 {00 ${speed} }"
176 | }
177 |
178 | def refresh() {
179 | [
180 | "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 100",
181 | "st rattr 0x${device.deviceNetworkId} 1 8 0"
182 | ]
183 | }
184 |
185 | def poll(){
186 | log.debug "Poll is calling refresh"
187 | refresh()
188 | }
189 |
190 | def setLevel(value) { setLevel(value,"0500") }
191 |
192 | def setLevel(value, speed) {
193 | log.debug value
194 | log.debug speed //.toString().padLeft(4, '0')
195 | state.lastOnValue = value
196 | speed = speed.toString().padLeft(4, '0')
197 | log.trace "setLevel($value)"
198 |
199 | def cmds = []
200 | if (value < 8.0) {
201 | log.debug "Value equals 0?"
202 | sendEvent(name: "switch", value: "off")
203 |
204 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {00 0500}"
205 | //cmds << "st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
206 | }
207 | else if (device.latestValue("switch") == "off") {
208 | sendEvent(name: "switch", value: "on")
209 | }
210 |
211 | sendEvent(name: "level", value: value)
212 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16)
213 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${speed} }"
214 | cmds
215 | }
216 |
217 | //def setLevel(value) {
218 |
219 | //}
220 |
221 |
222 | def configure() {
223 |
224 | String zigbeeId = swapEndianHex(device.hub.zigbeeId)
225 | log.debug "Confuguring Reporting and Bindings."
226 | def configCmds = [
227 |
228 | //Switch Reporting
229 | "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
230 | "send 0x${device.deviceNetworkId} 1 1", "delay 1000",
231 |
232 | //Level Control Reporting
233 | "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
234 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
235 |
236 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1500",
237 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500",
238 | ]
239 | return configCmds + refresh() // send refresh cmds as part of config
240 | }
241 |
242 | def uninstalled() {
243 |
244 | log.debug "uninstalled()"
245 | response("zcl rftd")
246 |
247 | }
248 |
249 | private getEndpointId() {
250 | //log.debug "Device.endpoint is $device.endpointId"
251 | new BigInteger(device.endpointId, 16).toString()
252 | }
253 |
254 | private hex(value, width=2) {
255 | def s = new BigInteger(Math.round(value).toString()).toString(16)
256 | while (s.size() < width) {
257 | s = "0" + s
258 | }
259 | s
260 | }
261 |
262 | private Integer convertHexToInt(hex) {
263 | Integer.parseInt(hex,16)
264 | }
265 |
266 |
267 | private String swapEndianHex(String hex) {
268 | reverseArray(hex.decodeHex()).encodeHex()
269 | }
270 |
271 | private byte[] reverseArray(byte[] array) {
272 | int i = 0;
273 | int j = array.length - 1;
274 | byte tmp;
275 | while (j > i) {
276 | tmp = array[j];
277 | array[j] = array[i];
278 | array[i] = tmp;
279 | j--;
280 | i++;
281 | }
282 | return array
283 | }
284 |
285 | def parseDescriptionAsMap(description) {
286 | (description - "read attr - ").split(",").inject([:]) { map, param ->
287 | def nameAndValue = param.split(":")
288 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/smartapps/pstuart/live-code-friday-virtual-device-manager.src/live-code-friday-virtual-device-manager.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Live Code Friday Virtual Device Manager
3 | *
4 | * Copyright 2015 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | * Tasks
16 | Create a dynamic pages interface
17 | Pick a device type
18 | add/delete that child
19 |
20 | */
21 | definition(
22 | name: "Live Code Friday Virtual Device Manager",
23 | namespace: "pstuart",
24 | author: "Patrick Stuart",
25 | description: "Live Code Friday Virtual Device Manager",
26 | category: "My Apps",
27 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
28 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
29 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
30 | singleInstance: true)
31 |
32 |
33 | preferences {
34 | page(name: "firstPage")
35 | page(name: "inputPage")
36 | page(name: "devicePage")
37 | page(name: "addDevicePage")
38 | page(name: "viewDevicePage")
39 | page(name: "deletePage")
40 | }
41 |
42 | def firstPage() {
43 | dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
44 | section("Main Menu") {
45 | paragraph "Version 1.2"
46 | href(page: "inputPage", title: "Let's Add Devices!")
47 |
48 | }
49 | /*
50 | section("Later") {
51 | paragraph "More to come..."
52 | }
53 | */
54 |
55 | }
56 | }
57 |
58 | def inputPage() {
59 | dynamicPage(name: "inputPage", title: "Choose What Device Type You Want To Add", nextPage:"firstPage") {
60 | section("Device Types") {
61 | href(page: "devicePage", title: "Switches", params: [device: "Generic Switch"])
62 | href(page: "devicePage", title: "Dimmers", params: [device: "Generic Dimmer"])
63 | href(page: "devicePage", title: "Contact Sensor", params: [device: "Generic Contact"])
64 |
65 | }
66 | section("Later") {
67 | paragraph "more devices coming soon"
68 | }
69 | section("Navigation") {
70 | href(page: "firstPage", title: "Main Menu")
71 | }
72 | }
73 | }
74 |
75 | def devicePage(params) {
76 | dynamicPage(name: "devicePage", title: "Devices", nextPage:"inputPage") {
77 | // Loop childDevices based on type
78 | // match up types
79 | def device = params.device
80 | log.debug "Hit Device Page with the selector device type $device"
81 | def deviceTitle = device + "s"
82 | if (device?.endsWith('ch') || device?.endsWith('s') || device?.endsWith('x')) {
83 | deviceTitle = device + "es"
84 | }
85 | log.debug "Device Title is $deviceTitle"
86 |
87 | section("Installed ${deviceTitle}") {
88 | def childDevices = getChildDevices()
89 |
90 | log.debug "The Child Devices are $childDevices"
91 | if (childDevices) {
92 | def devices = "Switches Installed:\r\nThis is a second line\r\n"
93 | log.debug "Inside childDevices if statement"
94 |
95 | childDevices.findAll { it.typeName == device }
96 | .each {
97 | log.debug "The child device id is $it.deviceNetworkId and the type is $it"
98 | def test = it.typeName
99 | log.debug "Testing $test"
100 | //def tempDevice = getChildDevice(it)
101 | //log.debug tempDevice
102 | //devices = devices + "test\r\n"
103 |
104 |
105 |
106 | switch(it.typeName) {
107 | case "Generic Switch" :
108 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId])
109 | break
110 | case "Generic Dimmer" :
111 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId])
112 | break
113 | case "Generic Contact" :
114 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId])
115 | break
116 | default : break
117 |
118 |
119 | }
120 |
121 | }
122 | } else {
123 | paragraph "No Virtual Generic Devices are Installed"
124 | }
125 | }
126 |
127 | section("Add A ${params.device}") { //${params.device}
128 | // List Switches getChildDevices()
129 |
130 | // Add A Switch addChildDevice()
131 | // View A Switch / Delete that switch go to switch view
132 | input("DeviceName", "text")
133 | href(page: "addDevicePage", title: "New $device", params: [type: device])
134 | }
135 |
136 | section("Navigation") {
137 | href(page: "firstPage", title: "Main Menu")
138 | }
139 | }
140 | }
141 |
142 | def addDevicePage(params) {
143 | dynamicPage(name: "addDevicePage", title: "New $params.type", nextPage:"devicePage") {
144 | section("New $params.type Add Result") {
145 | //add new virtual switch
146 | log.debug "Add Device Page hit with params $params and $settings.DeviceName"
147 | def newDeviceName = params.type
148 | if (settings.DeviceName) {
149 | newDeviceName = settings.DeviceName
150 | }
151 | def result = addChildDevice(params.type, newDeviceName)
152 | paragraph "$params.type Added ${result}"
153 |
154 | href(page: "devicePage", title: "Devices")
155 | }
156 |
157 | section("Navigation") {
158 | href(page: "firstPage", title: "Main Menu")
159 | }
160 | }
161 | }
162 |
163 | def viewDevicePage(params) {
164 | dynamicPage(name: "viewDevicePage", title: "Switch", nextPage:"devicePage") {
165 | def viewSwitch = getChildDevice(params.dni)
166 | section("$viewSwitch.name") {
167 | paragraph "Switch Details \r\nName: $viewSwitch.name\r\nType: $viewSwitch.typeName\r\nNetwork ID: $viewSwitch.deviceNetworkId\r\nStates\r\nSwitch: ${viewSwitch.currentState('switch').value}\r\n" // Create info about switch / child device
168 | log.debug viewSwitch.currentState('switch').value
169 | href(page: "deletePage", title: "Delete", params: [dni: params.dni])
170 | }
171 |
172 | section("Navigation") {
173 | href(page: "firstPage", title: "Main Menu")
174 | }
175 | }
176 | }
177 |
178 | def deletePage(params) {
179 | dynamicPage(name: "deletePage", title: "Delete", nextPage:"devicePage") {
180 | section("switch") {
181 | paragraph "Deleted Switch with DNI of $params.dni"
182 | log.debug "Deleting $params.dni"
183 | //def delete = getChildDevices().findAll { it?.contains(params.dni) }
184 | //log.debug delete
185 | def delete = getChildDevice(params.dni)
186 | //removeChildDevices(delete)
187 | deleteChildDevice(delete.deviceNetworkId)
188 |
189 | href(page: "switchPage", title: "Switches")
190 | }
191 |
192 | section("Navigation") {
193 | href(page: "firstPage", title: "Main Menu")
194 | }
195 | }
196 | }
197 |
198 | def installed() {
199 | log.debug "Installed with settings: ${settings}"
200 |
201 | initialize()
202 | }
203 |
204 | def updated() {
205 | log.debug "Updated with settings: ${settings}"
206 |
207 | unsubscribe()
208 | initialize()
209 | }
210 |
211 | def initialize() {
212 | // TODO: subscribe to attributes, devices, locations, etc.
213 | def switches = getChildDevices()
214 | switches.each {
215 | if (!it.currentValue('switch') ) {
216 | it.off()
217 | }
218 | }
219 | }
220 |
221 |
222 | def addChildDevice(params, deviceName) {
223 | //Get all devices installed as children
224 | def childDevices = getChildDevices() //.findAll{ it -> it.type == params } //Find device of type params.type
225 | //def collectDevices = childDevices.collect{ getChildDevice(it).type ?: 0}
226 | //log.debug "The result of collectDevices is $collectDevices"
227 |
228 | def gTypes = genericTypes()
229 | log.debug gTypes
230 | def subChildDevices = childDevices?.findAll { it -> it.typeName == params }
231 | log.debug "The subset of child devices is $subChildDevices based on type $params"
232 | def counter = subChildDevices?.size() + 1
233 | log.debug "$subChildDevices and counter is $counter"
234 |
235 | /*
236 | def counters = [:]
237 | childDevices.each {
238 | def childDevice = getChildDevice(it)
239 | log.debug "Child Device type is $childDevice.type"
240 | gTypes.each {
241 | if (it == childDevice.type) {
242 | def counter = ["name" : params.type, "counter" : counter++ ]
243 | }
244 | }
245 | } */
246 |
247 |
248 |
249 |
250 |
251 | //def counter = childDevices.size() + 1 //TODO Fix counter for each type
252 | def dni = "pstuartDevice_$counter" // TODO create random string /guid
253 | def newDeviceName = "$params $counter"
254 | if (deviceName != params) {
255 | newDeviceName = deviceName
256 | }
257 | log.debug newDeviceName
258 | log.debug dni
259 | log.debug params
260 | log.trace "just about to add childe device"
261 | def childDevice = addChildDevice("pstuart", params, dni, null, [name:newDeviceName])
262 | log.debug childDevice
263 | childDevice.off()
264 | return childDevice
265 | //return dni
266 |
267 |
268 | }
269 |
270 | def genericTypes() {
271 | def gTypes = [
272 | "Generic Switch",
273 | "Generic Contact",
274 | "Generic Dimmer",
275 | ]
276 | return gTypes
277 | }
--------------------------------------------------------------------------------
/smartapps/logger.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Xively Logger
3 | *
4 | * Author: patrick@patrickstuart.com
5 | * Date: 2014-04-14
6 | *
7 | *
8 | */
9 |
10 | // Automatically generated. Make future change here.
11 | definition(
12 | name: "Xively Logger",
13 | namespace: "ps",
14 | author: "patrick@patrickstuart.com",
15 | description: "Xively Logger",
16 | category: "My Apps",
17 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
18 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
19 |
20 | preferences {
21 | section("Log devices...") {
22 | input "temperatures", "capability.temperatureMeasurement", title: "Temperatures", required:false, multiple: true
23 | input "humidities", "capability.relativeHumidityMeasurement", title: "Humidities", required:false, multiple: true
24 | input "contacts", "capability.contactSensor", title: "Contacts", required: false, multiple: true
25 | input "illuminances", "capability.illuminanceMeasurement", title: "Illuminances", required: false, multiple: true
26 | input "motions", "capability.motionSensor", title: "Motions", required: false, multiple: true
27 | input "switches", "capability.switch", title: "Switches", required: false, multiple: true
28 | input "batteries", "capability.battery", title: "Batteries", required:false, multiple: true
29 | input "thermostats", "capability.thermostat", title: "Select thermostat", required: false, multiple: true
30 | }
31 |
32 | section ("Xively Info") {
33 | input "xi_apikey", "text", title: "Xively API Key"
34 | input "xi_feed", "number", title: "Xively Feed ID"
35 | }
36 | }
37 |
38 | def installed() {
39 | initialize()
40 | }
41 |
42 | def updated() {
43 | unsubscribe()
44 |
45 | initialize()
46 | }
47 |
48 | def initialize() {
49 | state.clear()
50 | // if(!state.subscribe) {
51 | unschedule(checkSensors)
52 | //schedule("0 */15 * * * ?", "checkSensors") //Check every 5 mins
53 | schedule("0 */15 * * * ?", "checkSensors")
54 | subscribe(app, appTouch)
55 | // state.subscribe = true
56 | // }
57 | /*
58 | subscribe(temperatures, "temperature", handleTemperatureEvent)
59 | subscribe(humidities, "humidities", handlehumidityEvent)
60 | subscribe(contacts, "contact", handleContactEvent)
61 | subscribe(accelerations, "acceleration", handleAccelerationEvent)
62 | subscribe(motions, "motion", handleMotionEvent)
63 | subscribe(switches, "switch", handleSwitchEvent)
64 | subscribe(thermostats, "temperature", handleThermostatTemperature)
65 | subscribe(thermostats, "humidity", handleThermostatHumidity)
66 | */
67 |
68 | //updateChannelInfo()
69 | //log.debug state.fieldMap
70 | }
71 |
72 | def appTouch(evt) {
73 | log.debug "appTouch: $evt"
74 | checkSensors()
75 | }
76 |
77 | /*
78 | def handleTemperatureEvent(evt) {
79 | log.debug "EVENT is $evt.displayName"
80 | log.debug "EVENT VALUE is $evt.value"
81 | //log.debug "Device ID is $evt.deviceId"
82 |
83 | logField("temperature", evt) { it.toString() }
84 | }
85 |
86 | def handlehumidityEvent(evt) {
87 | log.debug "EVENT is $evt.displayName"
88 | log.debug "EVENT VALUE is $evt.value"
89 |
90 | logField("humidity", evt) { it.toString() }
91 | }
92 |
93 | def handleContactEvent(evt) {
94 | log.debug "EVENT is $evt.displayName"
95 | log.debug "EVENT VALUE is $evt.value"
96 | logField("contact", evt) { it == "open" ? "1" : "0" }
97 | }
98 |
99 | def handleAccelerationEvent(evt) {
100 | log.debug "EVENT is $evt.displayName"
101 | log.debug "EVENT VALUE is $evt.value"
102 | logField("acceleration", evt) { it == "active" ? "1" : "0" }
103 | }
104 |
105 | def handleMotionEvent(evt) {
106 | log.debug "EVENT is $evt.displayName"
107 | log.debug "EVENT VALUE is $evt.value"
108 | logField("motion", evt) { it == "active" ? "1" : "0" }
109 | }
110 |
111 | def handleSwitchEvent(evt) {
112 | log.debug "EVENT is $evt.displayName"
113 | log.debug "EVENT VALUE is $evt.value"
114 | logField("switch", evt) { it == "on" ? "1" : "0" }
115 | }
116 |
117 | def handleThermostatTemperature(evt) {
118 | log.debug "EVENT is $evt.displayName"
119 | log.debug "EVENT VALUE is $evt.value"
120 | log.debug "TStat Temperature event: $evt.value"
121 | logField("thermostat.temperature", evt) { it.toString() }
122 | }
123 |
124 | def handleThermostatHumidity(evt) {
125 | log.debug "EVENT is $evt.displayName"
126 | log.debug "EVENT VALUE is $evt.value"
127 | log.debug "TStat Humidity event: $evt.value"
128 | logField("thermostat.humidity", evt) { it.toString() }
129 | }
130 | */
131 |
132 | def checkSensors() {
133 | //updateChannelInfo()
134 | //settings.sensors.each{ k -> log.debug k }
135 | //https://graph.api.smartthings.com/api/hubs/idHashString/devices
136 | //https://graph.api.smartthings.com/device/listJson
137 |
138 |
139 | def logitems = []
140 | //def states = [:]
141 | for (t in settings.temperatures) {
142 | //def deviceState = captureState(t)
143 | //log.debug "$t.name : $t.displayName : $deviceState.temperature"
144 | //def deviceID = t.id
145 | //states[deviceID] = deviceState
146 | //if (deviceID == "temperature") {
147 | //logitems.add([t.displayName, "temperature", deviceState.temperature] )
148 |
149 | logitems.add([t.displayName, "temperature", Double.parseDouble(t.latestValue("temperature").toString())] )
150 | state[t.displayName + ".temp"] = t.latestValue("temperature")
151 |
152 | //logField2("temperature", t.displayName, deviceState.temperature )
153 | //}
154 | }
155 | for (t in settings.humidities) {
156 | //def deviceState = captureState(t)
157 | //def deviceID = t.id
158 | //states[deviceID] = deviceState
159 | //logitems.add([t.displayName, "humidity", deviceState.humidity])
160 | logitems.add([t.displayName, "humidity", Double.parseDouble(t.latestValue("humidity").toString())] )
161 | state[t.displayName + ".humidity"] = t.latestValue("humidity")
162 | }
163 | for (t in settings.batteries) {
164 | //def deviceState = captureState(t)
165 | //log.debug deviceState
166 | //def deviceID = t.id
167 | //states[deviceID] = deviceState
168 | //logitems.add([t.displayName, "battery", deviceState.battery])
169 |
170 | logitems.add([t.displayName, "battery", Double.parseDouble(t.latestValue("battery").toString())] )
171 | state[t.displayName + ".battery"] = t.latestValue("battery")
172 | }
173 | //log.debug settings.batteries.size()
174 | for (t in settings.contacts) {
175 | //def deviceState = captureState(t)
176 | //log.debug deviceState
177 | //def deviceID = t.id
178 | //states[deviceID] = deviceState
179 | //logitems.add([t.displayName, "contact", deviceState.contact])
180 |
181 | logitems.add([t.displayName, "contact", t.latestValue("contact")] )
182 | state[t.displayName + ".contact"] = t.latestValue("contact")
183 | }
184 |
185 | for (t in settings.motions) {
186 | //def deviceState = captureState(t)
187 | //log.debug deviceState
188 | //def deviceID = t.id
189 | //states[deviceID] = deviceState
190 | //logitems.add([t.displayName, "motion", deviceState.motion])
191 |
192 | logitems.add([t.displayName, "motion", t.latestValue("motion")] )
193 | state[t.displayName + ".motion"] = t.latestValue("motion")
194 | }
195 |
196 | for (t in settings.illuminances) {
197 | //def deviceState = captureState(t)
198 | //log.debug deviceState
199 | //def deviceID = t.id
200 | //states[deviceID] = deviceState
201 | //logitems.add([t.displayName, "motion", deviceState.motion])
202 | //log.debug t.latestValue("illuminance")
203 | //log.debug (t.latestValue("illuminance") instanceof Double)
204 | log.debug t.displayName
205 | def x = new BigDecimal(t.latestValue("illuminance") ) // instanceof Double)
206 | log.debug x
207 | logitems.add([t.displayName, "illuminance", x] )
208 | state[t.displayName + ".illuminance"] = x
209 | }
210 |
211 | for (t in settings.switches) {
212 | //def deviceState = captureState(t)
213 | //log.debug deviceState
214 | //def deviceID = t.id
215 | //states[deviceID] = deviceState
216 | //logitems.add([t.displayName, "contact", deviceState.switch])
217 |
218 | logitems.add([t.displayName, "switch", t.latestValue("switch")] )
219 | state[t.displayName + ".switch"] = t.latestValue("switch")
220 | }
221 |
222 | for (t in settings.thermostats) {
223 | //def deviceState = captureState(t)
224 | //def deviceID = t.id + "tstat"
225 | //states[deviceID] = deviceState
226 | //logitems.add([t.displayName, "thermostat.temperature", deviceState.coolingSetpoint])
227 | //log.debug t
228 | logitems.add([t.displayName, "thermostat.coolingSetpoint", t.latestValue("coolingSetpoint")] )
229 | state[t.displayName + ".coolingSetpoint"] = t.latestValue("coolingSetpoint")
230 | }
231 |
232 | //log.debug states
233 | //log.debug logitems
234 | logField2(logitems)
235 |
236 | }
237 |
238 | private getFieldMap(channelInfo) {
239 | def fieldMap = [:]
240 | channelInfo?.findAll { it.key?.startsWith("field") }.each { fieldMap[it.value?.trim()] = it.key }
241 | return fieldMap
242 | }
243 |
244 | /*
245 | private updateChannelInfo() {
246 | log.debug "Retrieving channel info for ${channelId}"
247 |
248 | def url = "http://api.thingspeak.com/channels/${channelId}/feed.json?key=${channelKey}&results=0"
249 | httpGet(url) {
250 | response ->
251 | if (response.status != 200 ) {
252 | log.debug "ThingSpeak data retrieval failed, status = ${response.status}"
253 | } else {
254 | state.channelInfo = response.data?.channel
255 | }
256 | log.debug response.data
257 | }
258 |
259 | state.fieldMap = getFieldMap(state.channelInfo)
260 | }
261 | */
262 |
263 | /*
264 | private logField(capability, evt, Closure c) {
265 | log.debug "Got Log Request: $capability $evt.displayName $evt.value"
266 | def deviceName = evt.displayName.trim() + "." + capability
267 | def fieldNum = state.fieldMap[deviceName]
268 | if (!fieldNum) {
269 | log.debug "Device '${deviceName}' has no field"
270 | return
271 | }
272 |
273 | def value = c(evt.value)
274 | log.debug "Logging to channel ${channelId}, ${fieldNum}, value ${value}"
275 |
276 | def url = "http://api.thingspeak.com/update?key=${channelKey}&${fieldNum}=${value}"
277 | httpGet(url) {
278 | response ->
279 | if (response.status != 200 ) {
280 | log.debug "ThingSpeak logging failed, status = ${response.status}"
281 | }
282 | //log.debug response.data
283 | }
284 | }
285 | */
286 |
287 | private logField2(logItems) {
288 | def fieldvalues = ""
289 | log.debug logItems
290 |
291 | /*
292 | logItems.each() { item ->
293 | //log.debug item[0]
294 | //log.debug item[1]
295 | //log.debug item[2]
296 | //log.debug "Got Log Item: $item.get(0) $item.get(1) $item.get(2)"
297 |
298 | def deviceName = item[0] + "." + item[1]
299 | //log.debug deviceName
300 |
301 | def fieldNum = state.fieldMap[deviceName]
302 | //log.debug fieldNum
303 | if (!fieldNum) {
304 | log.debug "Device '${deviceName}' has no field"
305 | return
306 | }
307 | //log.debug fieldNum
308 |
309 |
310 |
311 | //build string &fieldnum=value
312 | fieldvalues += "&$fieldNum=" +item[2]
313 |
314 | //def value = c(evt.value)
315 | log.debug "Logging to channel ${channelId}, ${fieldNum}, value ${item[2]}"
316 |
317 | }
318 | log.debug "String to send is: $fieldvalues"
319 |
320 |
321 | def url = "http://api.thingspeak.com/update?key=${channelKey}${fieldvalues}"
322 | log.debug url
323 | httpGet(url) {
324 | response ->
325 | if (response.status != 200 ) {
326 | log.debug "ThingSpeak logging failed, status = ${response.status}"
327 | }
328 | //log.debug response.data
329 | }
330 |
331 | */
332 |
333 | def xivelyinfo = ""
334 | logItems.eachWithIndex() { item, i ->
335 | //need to build string {"id":"channel","current_value": "value"} plus comma
336 | def channelname = item[0].replace(" ","_") + "_" + item[1]
337 | xivelyinfo += "{\"id\":\"${channelname}\",\"current_value\":\"${item[2]}\"}"
338 | if (i.toInteger() + 1 < logItems.size())
339 | {
340 | xivelyinfo += ","
341 | }
342 |
343 | }
344 | log.debug xivelyinfo
345 | def uri = "https://api.xively.com/v2/feeds/${xi_feed}.json"
346 | //def json = "{\"version\":\"1.0.0\",\"datastreams\":[{\"id\":\"${channel}\",\"current_value\":\"${value}\"}]}"
347 | def json = "{\"version\":\"1.0.0\",\"datastreams\":[${xivelyinfo} ]}"
348 |
349 | def headers = [
350 | "X-ApiKey" : "${xi_apikey}"
351 | ]
352 |
353 | def params = [
354 | uri: uri,
355 | headers: headers,
356 | body: json
357 | ]
358 | log.debug params.body
359 | httpPutJson(params) {response -> parseHttpResponse(response)}
360 | }
361 |
362 | def parseHttpResponse(response) {
363 | log.debug "HTTP Response: ${response}"
364 | }
365 |
366 | def captureState(theDevice) {
367 | // def deviceAttributes = theDevice.supportedAttributes
368 | def deviceAttrValue = [:]
369 | for ( attr in theDevice.supportedAttributes ) {
370 | def attrName = "${attr}"
371 | def attrValue = theDevice.currentValue(attrName)
372 | deviceAttrValue[attrName] = attrValue
373 | }
374 | return deviceAttrValue
375 | }
376 |
--------------------------------------------------------------------------------
/smartapps/Ubi (Connect):
--------------------------------------------------------------------------------
1 | /**
2 | * ps_Ubi (Connect)
3 | *
4 | * Copyright 2014 patrick@patrickstuart.com
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | * BIG SHOUT OUT TO THE ECOBEE Example for showing me how OAUTH flow can work...
16 | */
17 | definition(
18 | name: "ps_Ubi (Connect)",
19 | namespace: "ps",
20 | author: "patrick@patrickstuart.com",
21 | description: "Connect to Ubi Portal",
22 | category: "My Apps",
23 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
24 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") {
25 |
26 | }
27 |
28 |
29 | preferences {
30 | page(name: "auth", title: "ubi", nextPage:"deviceList", content:"authPage", uninstall: true)
31 | page(name: "deviceList", title: "ubi", content:"ubiDeviceList", install:true)
32 | }
33 |
34 | mappings {
35 | path("/swapToken") {
36 | action: [
37 | GET: "swapToken"
38 | ]
39 | }
40 | }
41 |
42 | def authPage()
43 | {
44 | log.debug "Version 1.0.2"
45 | log.debug "authPage()"
46 |
47 | if(!atomicState.accessToken)
48 | {
49 | log.debug "about to create access token"
50 | createAccessToken()
51 | atomicState.accessToken = state.accessToken
52 | }
53 |
54 |
55 | def description = "Required"
56 | def uninstallAllowed = false
57 | def oauthTokenProvided = false
58 |
59 | if(atomicState.authToken)
60 | {
61 | if(true)
62 | {
63 | description = "You are connected."
64 | uninstallAllowed = true
65 | oauthTokenProvided = true
66 | }
67 | else
68 | {
69 | description = "Required"
70 | oauthTokenProvided = false
71 | }
72 | }
73 |
74 | def redirectUrl = oauthInitUrl()
75 |
76 | log.debug "RedirectUrl = ${redirectUrl}"
77 |
78 | // get rid of next button until the user is actually auth'd
79 | log.debug "_______AUTH______ ${atomicState.authToken}"
80 | log.debug oauthTokenProvided
81 |
82 | if (!oauthTokenProvided) {
83 |
84 | return dynamicPage(name: "auth", title: "Login", nextPage:null, uninstall:uninstallAllowed) {
85 | section(){
86 | paragraph "Tap below to log in to the ubi service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
87 | href url:redirectUrl, style:"embedded", required:true, title:"ubi", description:description
88 | }
89 | }
90 |
91 | } else {
92 |
93 | return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", uninstall:uninstallAllowed) {
94 | section(){
95 | paragraph "Tap Next to continue to setup your ubi."
96 | href url:redirectUrl, style:"embedded", state:"complete", title:"ubi", description:description
97 | }
98 | }
99 |
100 | }
101 |
102 | }
103 |
104 | def ubiDeviceList()
105 | {
106 | log.debug "ubiDeviceList()"
107 |
108 | def stats = getUbis()
109 |
110 | log.debug "device list: $stats"
111 |
112 | def p = dynamicPage(name: "deviceList", title: "Select Your Ubis", uninstall: true) {
113 | section(""){
114 | paragraph "Tap below to see the list of Ubis available in your Ubi account and select the ones you want to connect to SmartThings."
115 | input(name: "ubis", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats])
116 | }
117 | }
118 |
119 | log.debug "list p: $p"
120 | return p
121 | }
122 |
123 | def getUbis()
124 | {
125 | log.debug "getting device list"
126 |
127 | def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}'
128 |
129 | def deviceListParams = [
130 | uri: "https://api.theubi.com",
131 | path: "/v2/ubi/list",
132 | headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
133 | query: [format: 'json', body: requestBody]
134 | ]
135 |
136 | log.debug "_______AUTH______ ${atomicState.authToken}"
137 | log.debug "device list params: $deviceListParams"
138 |
139 | def stats = [:]
140 | httpGet(deviceListParams) { resp ->
141 |
142 | if(resp.status == 200)
143 | {
144 | resp.data.ubiList.each { stat ->
145 | def dni = [ app.id, stat.identifier ].join('.')
146 | stats[dni] = getUbiDisplayName(stat)
147 | }
148 | }
149 | else
150 | {
151 | log.debug "http status: ${resp.status}"
152 |
153 | //refresh the auth token
154 | if (resp.status == 500 && resp.data.status.code == 14)
155 | {
156 | log.debug "Storing the failed action to try later"
157 | data.action = "getUbis"
158 | log.debug "Refreshing your auth_token!"
159 | refreshAuthToken()
160 | }
161 | else
162 | {
163 | log.error "Authentication error, invalid authentication method, lack of credentials, etc."
164 | }
165 | }
166 | }
167 |
168 | log.debug "thermostats: $stats"
169 |
170 | return stats
171 | }
172 |
173 |
174 | def getUbiDisplayName(stat)
175 | {
176 | log.debug "getUbiDisplayName"
177 | if(stat?.name)
178 | {
179 | return stat.name.toString()
180 | }
181 |
182 | return ${stat.identifier}
183 | }
184 |
185 | def installed() {
186 | log.debug "Installed with settings: ${settings}"
187 |
188 | initialize()
189 | }
190 |
191 | def updated() {
192 | log.debug "Updated with settings: ${settings}"
193 |
194 | unsubscribe()
195 | initialize()
196 | }
197 |
198 | def initialize() {
199 | // TODO: subscribe to attributes, devices, locations, etc.
200 | log.debug "initialize"
201 | def devices = ubis.collect { dni ->
202 |
203 | def d = getChildDevice(dni)
204 |
205 | if(!d)
206 | {
207 | d = addChildDevice(getChildNamespace(), getChildName(), dni)
208 | log.debug "created ${d.displayName} with id $dni"
209 | }
210 | else
211 | {
212 | log.debug "found ${d.displayName} with id $dni already exists"
213 | }
214 |
215 | return d
216 | }
217 |
218 | log.debug "created ${devices.size()} ubis"
219 |
220 | def delete
221 | // Delete any that are no longer in settings
222 | if(!ubis)
223 | {
224 | log.debug "delete ubis"
225 | delete = getAllChildDevices()
226 | }
227 | else
228 | {
229 | delete = getChildDevices().findAll { !ubis.contains(it.deviceNetworkId) }
230 | }
231 |
232 | log.debug "deleting ${delete.size()} ubis"
233 | delete.each { deleteChildDevice(it.deviceNetworkId) }
234 |
235 | atomicState.thermostatData = [:]
236 |
237 | pollHandler()
238 |
239 | // schedule ("0 0/15 * 1/1 * ? *", pollHandler)
240 | }
241 |
242 |
243 | def oauthInitUrl()
244 | {
245 | log.debug "oauthInitUrl"
246 | // def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123"
247 | def stcid = getSmartThingsClientId();
248 |
249 | atomicState.oauthInitState = UUID.randomUUID().toString()
250 |
251 | def oauthParams = [
252 | response_type: "code",
253 | scope: "",
254 | client_id: stcid,
255 | client_secret: "07c98380-441c-4898-9880-eb3a70144832",
256 | state: atomicState.oauthInitState,
257 | redirect_uri: buildRedirectUrl()
258 | ]
259 | log.debug oauthParams
260 | return "http://test.theubi.com/oauth/authorize?" + toQueryString(oauthParams)
261 | }
262 |
263 | def buildRedirectUrl()
264 | {
265 | log.debug "buildRedirectUrl ${serverUrl}"
266 | // return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}"
267 | return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken"
268 | }
269 |
270 | private refreshAuthToken() {
271 | log.debug "refreshing auth token"
272 | //debugEvent("refreshing OAUTH token", true)
273 |
274 | def stcid = getSmartThingsClientId()
275 |
276 | def refreshParams = [
277 | method: 'POST',
278 | uri: "http://test.theubi.com",
279 | path: "/oauth/token",
280 | query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid],
281 |
282 | //data?.refreshToken
283 | ]
284 |
285 | log.debug "Patrick: refreshParams= $refreshParams"
286 |
287 | //changed to httpPost
288 | try{
289 | def jsonMap
290 | httpPost(refreshParams) { resp ->
291 |
292 | if(resp.status == 200)
293 | {
294 | log.debug "Token refreshed...calling saved RestAction now!"
295 |
296 | //debugEvent("Token refreshed ... calling saved RestAction now!", true)
297 |
298 | log.debug resp
299 |
300 | jsonMap = resp.data
301 |
302 | if (resp.data) {
303 |
304 | log.debug resp.data
305 | //debugEvent ("Response = ${resp.data}", true)
306 |
307 | atomicState.refreshToken = resp?.data?.refresh_token
308 | atomicState.authToken = resp?.data?.access_token
309 |
310 | //debugEvent ("Refresh Token = ${atomicState.refreshToken}", true)
311 | //debugEvent ("OAUTH Token = ${atomicState.authToken}", true)
312 |
313 | if (data?.action && data?.action != "") {
314 | log.debug data.action
315 |
316 | "{data.action}"()
317 |
318 | //remove saved action
319 | data.action = ""
320 | }
321 |
322 | }
323 | data.action = ""
324 | }
325 | else
326 | {
327 | log.debug "refresh failed ${resp.status} : ${resp.status.code}"
328 | }
329 | }
330 | log.debug "Patrick: $jsonMap"
331 | atomicState.refreshToken = jsonMap.refresh_token
332 | atomicState.authToken = jsonMap.access_token
333 | }
334 | catch(Exception e)
335 | {
336 | log.debug "caught exception refreshing auth token: " + e
337 | }
338 | log.debug "_____AUTH_____ ${atomicState.authToken}"
339 | }
340 |
341 | def swapToken()
342 | {
343 | log.debug "swapping token: $params"
344 | log.debug "_____AUTH_____ ${atomicState.authToken}"
345 | //debugEvent ("swapping token: $params", true)
346 |
347 | def code = params.code
348 | def oauthState = params.state
349 |
350 | // TODO: verify oauthState == atomicState.oauthInitState
351 |
352 |
353 |
354 | def stcid = getSmartThingsClientId()
355 |
356 | def tokenParams = [
357 | grant_type: "authorization_code",
358 | code: params.code,
359 | client_id: stcid,
360 | client_secret: "07c98380-441c-4898-9880-eb3a70144832",
361 | redirect_uri: buildRedirectUrl()
362 | ]
363 |
364 |
365 | def tokenUrl = "http://test.theubi.com/oauth/token?" + toQueryString(tokenParams)
366 |
367 | log.debug "SCOTT: swapping token $params"
368 |
369 | def jsonMap
370 | httpPost(uri:tokenUrl) { resp ->
371 | jsonMap = resp.data
372 | }
373 |
374 | log.debug "SCOTT: swapped token for $jsonMap"
375 | //debugEvent ("swapped token for $jsonMap", true)
376 |
377 | atomicState.refreshToken = jsonMap.refresh_token
378 | atomicState.authToken = jsonMap.access_token
379 | log.debug atomicState
380 | def html = """
381 |
382 |
383 |
384 |
385 | Withings Connection
386 |
436 |
437 |
438 |
439 |
440 |

441 |

442 |
Your ubi Account is now connected to SmartThings!
443 |
Click 'Done' to finish setup.
444 |
445 |
446 |
447 | """
448 |
449 | render contentType: 'text/html', data: html
450 | }
451 |
452 |
453 |
454 | def toQueryString(Map m)
455 | {
456 | return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
457 | }
458 |
459 | def getServerUrl() { return "https://graph.api.smartthings.com" }
460 | def getSmartThingsClientId() { "55c9dd16-9268-4e1a-ab92-0fa5d876a987" }
461 |
--------------------------------------------------------------------------------
/devicetypes/nest.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * ps_Nest Thermostat
3 | *
4 | * Copyright 2014 Patrick Stuart
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 | * in compliance with the License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 | * for the specific language governing permissions and limitations under the License.
14 | *
15 | *
16 | * Original Author: dianoga7@3dgo.net
17 | * Author: patrick@patrickstuart.com
18 | * Original Code: https://github.com/smartthings-users/device-type.nest
19 | *
20 | * INSTALLATION
21 | * =========================================
22 | * 1) Create a new device type (https://graph.api.smartthings.com/ide/devices)
23 | * Name: Nest
24 | * Author: dianoga7@3dgo.net
25 | * Capabilities:
26 | * Polling
27 | * Relative Humidity Measurement
28 | * Thermostat
29 | * Custom Attributes:
30 | * presence
31 | * Custom Commands:
32 | * away
33 | * present
34 | * setPresence
35 | *
36 | * 2) Create a new device (https://graph.api.smartthings.com/device/list)
37 | * Name: Your Choice
38 | * Device Network Id: Your Choice
39 | * Type: Nest (should be the last option)
40 | * Location: Choose the correct location
41 | * Hub/Group: Leave blank
42 | *
43 | * 3) Update device preferences
44 | * Click on the new device to see the details.
45 | * Click the edit button next to Preferences
46 | * Fill in your information.
47 | * To find your serial number, login to http://home.nest.com. Click on the thermostat
48 | * you want to control. Under settings, go to Technical Info. Your serial number is
49 | * the second item.
50 | *
51 | * 4) That's it, you're done.
52 | *
53 | * Copyright (C) 2013 Brian Steere
54 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this
55 | * software and associated documentation files (the "Software"), to deal in the Software
56 | * without restriction, including without limitation the rights to use, copy, modify,
57 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
58 | * permit persons to whom the Software is furnished to do so, subject to the following
59 | * conditions: The above copyright notice and this permission notice shall be included
60 | * in all copies or substantial portions of the Software.
61 | *
62 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
63 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
64 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
65 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
66 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
67 | * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
68 | */
69 |
70 | preferences {
71 | input("username", "text", title: "Username", description: "Your Nest username (usually an email address)")
72 | input("password", "password", title: "Password", description: "Your Nest password")
73 | input("serial", "text", title: "Serial #", description: "The serial number of your thermostat")
74 | }
75 |
76 | // for the UI
77 | metadata {
78 | definition (name: "Nest Thermostat", author: "patrick@patrickstuart.com") {
79 | capability "Polling"
80 | capability "Relative Humidity Measurement"
81 | capability "Thermostat"
82 | capability "Actuator"
83 | capability "Sensor"
84 | capability "Refresh"
85 | capability "Configuration"
86 | capability "Location Mode"
87 |
88 | attribute "presence", "string"
89 |
90 | command "away"
91 | command "present"
92 | command "setPresence"
93 | command "setTempUp"
94 | command "setTempDown"
95 | command "setTempUpHeat"
96 | command "setTempDownHeat"
97 | }
98 | simulator {
99 |
100 | }
101 |
102 | tiles {
103 | valueTile("temperature", "device.temperature", width: 1, height: 1, canChangeIcon: false) {
104 | state("temperature", label: '${currentValue}°', unit:"F", backgroundColors: [
105 | [value: 31, color: "#153591"],
106 | [value: 44, color: "#1e9cbb"],
107 | [value: 59, color: "#90d2a7"],
108 | [value: 74, color: "#44b621"],
109 | [value: 84, color: "#f1d801"],
110 | [value: 95, color: "#d04e00"],
111 | [value: 96, color: "#bc2323"]
112 | ]
113 | )
114 | }
115 |
116 | standardTile("TempUp", "device.button") { state "Cool", label:'${name}', action:"setTempUp", icon: "st.thermostat.thermostat-up", backgroundColor: '#003cec'}
117 | standardTile("TempDown", "device.button") { state "Cool", label:'${name}', action:"setTempDown", icon: "st.thermostat.thermostat-down", backgroundColor: '#003cec'}
118 |
119 | standardTile("TempUpHeat", "device.button") { state "Heat", label:'${name}', action:"setTempUpHeat", icon: "st.thermostat.thermostat-up", backgroundColor: '#e14902'}
120 | standardTile("TempDownHeat", "device.button") { state "Heat", label:'${name}', action:"setTempDownHeat", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902'}
121 |
122 |
123 | standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
124 | state "heat", label:'', action:"thermostat.heat", icon: "st.thermostat.heat", backgroundColor: '#E14902'
125 | state "cool", label:'', action:"thermostat.cool", icon: "st.thermostat.cool", backgroundColor: '#003CEC'
126 | state "range", label:'', action:"thermostat.auto", icon: "st.thermostat.auto", backgroundColor: '#000000'
127 | state "off", label:'', action:"thermostat.off", icon: "st.thermostat.heating-cooling-off"
128 | }
129 | standardTile("thermostatFanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
130 | state "auto", label:'', action:"thermostat.fanOn", icon: "st.thermostat.fan-auto"
131 | state "on", label:'', action:"thermostat.fanCirculate", icon: "st.thermostat.fan-on"
132 | state "circulate", label:'', action:"thermostat.fanAuto", icon: "st.thermostat.fan-circulate"
133 | }
134 | valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
135 | state "default", label:'${currentValue}°', unit:"F", backgroundColor:"#ffffff", icon:""
136 | }
137 |
138 | valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
139 | state "default", label:'${currentValue}°', unit:"F", backgroundColor:"#ffffff", icon:""
140 | }
141 |
142 | valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") {
143 | state "default", label:'${currentValue}%', unit:"Humidity"
144 | }
145 | standardTile("presence", "device.presence", inactiveLabel: false, decoration: "flat") {
146 | state "present", label:'${name}', action:"away", icon: "st.Home.home2"
147 | state "away", label:'${name}', action:"present", icon: "st.Transportation.transportation5"
148 | }
149 | standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
150 | state "default", action:"polling.poll", icon:"st.secondary.refresh"
151 | }
152 | main "temperature"
153 | details(["temperature", "thermostatMode", "thermostatFanMode", "TempUp", "TempDown", //"coolSliderControl",
154 | "coolingSetpoint", "TempUpHeat", "TempDownHeat", "heatingSetpoint", "humidity", "presence", "refresh"])
155 | }
156 | }
157 |
158 | // parse events into attributes
159 | def parse(String description) {
160 | log.debug "Recieved a parse request for: $description"
161 |
162 | }
163 |
164 | def setTempUp() {
165 | def newtemp = device.currentValue("coolingSetpoint").toInteger() + 1
166 | sendEvent(name: 'coolingSetpoint', value: newtemp)
167 | setTargetTemp(newtemp)
168 | }
169 |
170 | def setTempDown() {
171 | def newtemp = device.currentValue("coolingSetpoint").toInteger() - 1
172 | sendEvent(name: 'coolingSetpoint', value: newtemp)
173 | setTargetTemp(newtemp)
174 | }
175 |
176 | def setTempUpHeat() {
177 | def newtemp = device.currentValue("heatingSetpoint").toInteger() + 1
178 | sendEvent(name: 'heatingSetpoint', value: newtemp)
179 | setCoolingSetpoint(newtemp)
180 | }
181 |
182 | def setTempDownHeat() {
183 | def newtemp = device.currentValue("heatingSetpoint").toInteger() - 1
184 | sendEvent(name: 'heatingSetpoint', value: newtemp)
185 | setHeatingSetpoint(newtemp)
186 | }
187 |
188 |
189 | // handle commands
190 | def setHeatingSetpoint(temp) {
191 | setTargetTemp(temp)
192 | }
193 |
194 | def setCoolingSetpoint(temp) {
195 | setTargetTemp(temp)
196 | }
197 |
198 | def setTargetTemp(temp) {
199 | def tmode = device.currentValue("thermostatMode")
200 | if (tmode == "range")
201 | {
202 | log.debug "in auto mode about to send to api"
203 | api('temperature', [
204 | 'target_change_pending': true,
205 | 'target_temperature_low': fToC(device.currentValue("heatingSetpoint")),
206 | 'target_temperature_high': fToC(device.currentValue("coolingSetpoint")),
207 | 'target_temperature': fToC(temp)
208 | ]) { }
209 | }
210 | else
211 | {
212 | if (tmode == "heat")
213 | {
214 | api('temperature', ['target_change_pending': true, 'target_temperature': fToC(device.currentValue("heatingSetpoint"))]) {
215 | }
216 | }
217 | else if (tmode == "cool")
218 | {
219 | api('temperature', ['target_change_pending': true, 'target_temperature': fToC(device.currentValue("coolingSetpoint"))]) {
220 | }
221 | }
222 | }
223 | }
224 |
225 |
226 |
227 |
228 | def off() {
229 | setThermostatMode('off')
230 | log.debug "off"
231 | }
232 |
233 | def heat() {
234 | setThermostatMode('heat')
235 | log.debug "heat"
236 | }
237 |
238 | def emergencyHeat() {
239 | setThermostatMode('heat')
240 | log.debug "eheat"
241 | }
242 |
243 | def cool() {
244 | setThermostatMode('cool')
245 | log.debug "cool"
246 | }
247 |
248 | def auto() {
249 | def tmode = device.currentValue("thermostatMode")
250 | setThermostatMode(tmode)
251 | log.debug "auto Mode hit"
252 | }
253 |
254 | def setThermostatMode(mode) {
255 | log.debug "mode is $mode"
256 | // rotate through 4 options heat,cool, range, off
257 | switch (mode)
258 | {
259 | case 'heat' :
260 | mode = "cool"
261 | break
262 | case 'cool' :
263 | mode = "range"
264 | break
265 | case 'range' :
266 | mode = "off"
267 | break
268 | case 'off' :
269 | mode = "heat"
270 | break
271 | default :
272 | mode = "off"
273 | break
274 | }
275 |
276 | api('thermostat_mode', ['target_change_pending': true, 'target_temperature_type': mode]) {
277 | sendEvent(name: 'thermostatMode', value: mode)
278 | }
279 | }
280 |
281 | def fanOn() {
282 | setThermostatFanMode('on')
283 | }
284 |
285 | def fanAuto() {
286 | setThermostatFanMode('auto')
287 | }
288 |
289 | def fanCirculate() {
290 | setThermostatFanMode('circulate')
291 | }
292 |
293 | def setThermostatFanMode(mode) {
294 | def modes = [
295 | on: ['fan_mode': 'on'],
296 | auto: ['fan_mode': 'auto'],
297 | circulate: ['fan_mode': 'duty-cycle', 'fan_duty_cycle': 900]
298 | ]
299 |
300 | api('fan_mode', modes.getAt(mode)) {
301 | sendEvent(name: 'thermostatFanMode', value: mode)
302 | }
303 | }
304 |
305 | def away() {
306 | log.debug "Away Nest"
307 | setPresence('away')
308 | }
309 |
310 | def present() {
311 | log.debug "Home Nest"
312 | setPresence('present')
313 | }
314 |
315 | def setPresence(status) {
316 | log.debug "Status: $status"
317 | api('presence', ['away': status == 'away', 'away_timestamp': new Date().getTime(), 'away_setter': 0]) {
318 | sendEvent(name: 'presence', value: status)
319 | }
320 | }
321 |
322 | def refresh() {
323 | log.debug "nest refresh"
324 | }
325 |
326 | def poll() {
327 | log.debug "Executing 'poll'"
328 | api('status', []) {
329 | log.debug data.shared
330 | data.device = it.data.device.getAt(settings.serial)
331 | data.shared = it.data.shared.getAt(settings.serial)
332 | data.structureId = it.data.link.getAt(settings.serial).structure.tokenize('.')[1]
333 | data.structure = it.data.structure.getAt(data.structureId)
334 |
335 | data.device.fan_mode = data.device.fan_mode == 'duty-cycle'? 'circulate' : data.device.fan_mode
336 | data.structure.away = data.structure.away? 'away' : 'present'
337 |
338 | sendEvent(name: 'humidity', value: data.device.current_humidity)
339 | sendEvent(name: 'temperature', value: cToF((data.shared.current_temperature) as Double).round(), state: data.device.target_temperature_type)
340 | sendEvent(name: 'thermostatFanMode', value: data.device.fan_mode)
341 | sendEvent(name: 'thermostatMode', value: data.shared.target_temperature_type)
342 | //if temp type is ranged otherwise use target_temp
343 | if (data.shared.target_temperature_type == "range")
344 | {
345 | sendEvent(name: 'coolingSetpoint', value: cToF((data.shared.target_temperature_high) as Double).round())
346 | sendEvent(name: 'heatingSetpoint', value: cToF((data.shared.target_temperature_low) as Double).round())
347 | }
348 | else if (data.shared.target_temperature_type == "cool")
349 | {
350 | sendEvent(name: 'coolingSetpoint', value: cToF((data.shared.target_temperature) as Double).round())
351 | }
352 | else if (data.shared.target_temperature_type == "heat")
353 | {
354 | sendEvent(name: 'heatingSetpoint', value: cToF((data.shared.target_temperature) as Double).round())
355 | }
356 | else
357 | {
358 | sendEvent(name: 'coolingSetpoint', value: 0) //changed for auto mode for heat/cool?
359 | sendEvent(name: 'heatingSetpoint', value: 0)
360 | }
361 |
362 | sendEvent(name: 'presence', value: data.structure.away)
363 |
364 | //ambient_temperature_f?
365 | //has_leaf?
366 | //name = tstat name
367 | }
368 | }
369 |
370 | def api(method, args = [], success = {}) {
371 | if(!isLoggedIn()) {
372 | log.debug "Need to login"
373 | login(method, args, success)
374 | return
375 | }
376 |
377 | def methods = [
378 | 'status': [uri: "/v2/mobile/${data.auth.user}", type: 'get'],
379 | 'fan_mode': [uri: "/v2/put/device.${settings.serial}", type: 'post'],
380 | 'thermostat_mode': [uri: "/v2/put/shared.${settings.serial}", type: 'post'],
381 | 'temperature': [uri: "/v2/put/shared.${settings.serial}", type: 'post'],
382 | 'presence': [uri: "/v2/put/structure.${data.structureId}", type: 'post']
383 | ]
384 |
385 | def request = methods.getAt(method)
386 | log.debug method
387 | //Potentially this is the only spot that triggers from Mode Changes
388 | log.debug "Current mode = ${location.mode}"
389 |
390 | log.debug "Logged in"
391 | log.debug "Arguments to send are : $args"
392 | doRequest(request.uri, args, request.type, success)
393 | }
394 |
395 | // Need to be logged in before this is called. So don't call this. Call api.
396 | def doRequest(uri, args, type, success) {
397 | log.debug "Calling $type : $uri : $args"
398 |
399 | if(uri.charAt(0) == '/') {
400 | uri = "${data.auth.urls.transport_url}${uri}"
401 | }
402 |
403 | def params = [
404 | uri: uri,
405 | headers: [
406 | 'X-nl-protocol-version': 1,
407 | 'X-nl-user-id': data.auth.userid,
408 | 'Authorization': "Basic ${data.auth.access_token}"
409 | ],
410 | body: args
411 | ]
412 |
413 | try {
414 | if(type == 'post') {
415 | httpPostJson(params, success)
416 | } else if (type == 'get') {
417 | httpGet(params, success)
418 | }
419 | } catch (Throwable e) {
420 | login()
421 | }
422 | }
423 |
424 | def login(method = null, args = [], success = {}) {
425 | def params = [
426 | uri: 'https://home.nest.com/user/login',
427 | body: [username: settings.username, password: settings.password]
428 | ]
429 |
430 | httpPost(params) {response ->
431 | data.auth = response.data
432 | data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime()
433 | log.debug data.auth
434 |
435 | api(method, args, success)
436 | }
437 | }
438 |
439 | def isLoggedIn() {
440 | if(!data.auth) {
441 | log.debug "No data.auth"
442 | return false
443 | }
444 |
445 | def now = new Date().getTime();
446 | return data.auth.expires_in > now
447 | }
448 |
449 | def cToF(temp) {
450 | return temp * 1.8 + 32
451 | }
452 |
453 | def fToC(temp) {
454 | return (temp - 32) / 1.8
455 | }
456 |
457 | def configure() {
458 | log.debug "Configure() nest hit"
459 | }
460 |
461 | def switchMode() {
462 | def currentMode = device.currentState("thermostatMode")?.value
463 | log.debug "nest current mode is $currentMode"
464 | }
465 |
--------------------------------------------------------------------------------
/devicetypes/smartsense multi + graph.groovy:
--------------------------------------------------------------------------------
1 | metadata {
2 |
3 | definition (name: "SmartSense Multi + Graph", namespace: "smartthings", author: "SmartThings") {
4 | capability "Three Axis"
5 | capability "Contact Sensor"
6 | capability "Acceleration Sensor"
7 | capability "Signal Strength"
8 | capability "Temperature Measurement"
9 | capability "Sensor"
10 | capability "Battery"
11 | }
12 |
13 | preferences {
14 | input("TempAdjust", "Double", title: "Temp Adjust", description: "Adjust your temp reading +/-")
15 | }
16 |
17 | simulator {
18 | status "open": "zone report :: type: 19 value: 0031"
19 | status "closed": "zone report :: type: 19 value: 0030"
20 |
21 | status "acceleration": "acceleration: 1, rssi: 0, lqi: 0"
22 | status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0"
23 |
24 | for (int i = 10; i <= 50; i += 10) {
25 | status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100, rssi: 100, lqi: 255"
26 | }
27 |
28 | status "temp 72F": "contactSate: 0, accelerationState: 0, temp: 72 F, battery: 100, rssi: 100, lqi:255"
29 |
30 | // kinda hacky because it depends on how it is installed
31 | status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255"
32 | status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255"
33 | status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255"
34 | status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255"
35 |
36 |
37 | }
38 |
39 | tiles {
40 | standardTile("contact", "device.contact", width: 2, height: 2) {
41 | state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
42 | state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
43 | }
44 | standardTile("acceleration", "device.acceleration") {
45 | state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
46 | state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
47 | }
48 | valueTile("temperature", "device.temperature") {
49 | state("temperature", label:'${currentValue}°',
50 | backgroundColors:[
51 | [value: 31, color: "#153591"],
52 | [value: 44, color: "#1e9cbb"],
53 | [value: 59, color: "#90d2a7"],
54 | [value: 74, color: "#44b621"],
55 | [value: 84, color: "#f1d801"],
56 | [value: 95, color: "#d04e00"],
57 | [value: 96, color: "#bc2323"]
58 | ]
59 | )
60 | }
61 | chartTile(name: "temperatureChart", attribute: "device.temperature")
62 | valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) {
63 | state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
64 | }
65 | valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
66 | state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
67 | [value: 5, color: "#BC2323"],
68 | [value: 10, color: "#D04E00"],
69 | [value: 15, color: "#F1D801"],
70 | [value: 16, color: "#FFFFFF"]
71 | ]*/
72 | }
73 | /*
74 | valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
75 | state "lqi", label:'${currentValue}% signal', unit:""
76 | }
77 | */
78 |
79 | main(["contact", "acceleration", "temperature"])
80 | details(["contact", "acceleration", "temperature", "3axis", "battery", /*"lqi",*/ "temperatureChart"])
81 | }
82 | }
83 |
84 | def parse(String description) {
85 | log.debug "Parse got this info: $description"
86 | def results
87 |
88 | if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) {
89 | results = parseSingleMessage(description)
90 | }
91 | else if (description == 'updated') {
92 | //TODO is there a better way to handle this like the other device types?
93 | results = parseOtherMessage(description)
94 | }
95 | else {
96 | results = parseMultiSensorMessage(description)
97 | }
98 | log.debug "Parse returned $results.descriptionText"
99 | return results
100 |
101 | }
102 |
103 | private Map parseSingleMessage(description) {
104 |
105 | def name = parseName(description)
106 | def value = parseValue(description)
107 | def linkText = getLinkText(device)
108 | def descriptionText = parseDescriptionText(linkText, value, description)
109 | def handlerName = value == 'open' ? 'opened' : value
110 | def isStateChange = isStateChange(device, name, value)
111 |
112 | def results = [
113 | name: name,
114 | value: value,
115 | unit: null,
116 | linkText: linkText,
117 | descriptionText: descriptionText,
118 | handlerName: handlerName,
119 | isStateChange: isStateChange,
120 | displayed: displayed(description, isStateChange)
121 | ]
122 | log.debug "Parse results for $device: $results"
123 |
124 | results
125 | }
126 |
127 | //TODO this just to handle 'updated' for now - investigate better way to do this
128 | private Map parseOtherMessage(description) {
129 | def name = null
130 | def value = description
131 | def linkText = getLinkText(device)
132 | def descriptionText = description
133 | def handlerName = description
134 | def isStateChange = isStateChange(device, name, value)
135 |
136 | def results = [
137 | name: name,
138 | value: value,
139 | unit: null,
140 | linkText: linkText,
141 | descriptionText: descriptionText,
142 | handlerName: handlerName,
143 | isStateChange: isStateChange,
144 | displayed: displayed(description, isStateChange)
145 | ]
146 | log.debug "Parse results for $device: $results"
147 |
148 | results
149 | }
150 |
151 | private List parseMultiSensorMessage(description) {
152 | def results = []
153 | if (isAccelerationMessage(description)) {
154 | results = parseAccelerationMessage(description)
155 | }
156 | else if (isContactMessage(description)) {
157 | results = parseContactMessage(description)
158 | }
159 | else if (isRssiLqiMessage(description)) {
160 | results = parseRssiLqiMessage(description)
161 | }
162 | else if (isOrientationMessage(description)) {
163 | results = parseOrientationMessage(description)
164 | }
165 |
166 | results
167 | }
168 |
169 | private List parseAccelerationMessage(String description) {
170 | def results = []
171 | def parts = description.split(',')
172 | parts.each { part ->
173 | part = part.trim()
174 | if (part.startsWith('acceleration:')) {
175 | results << getAccelerationResult(part, description)
176 | }
177 | else if (part.startsWith('rssi:')) {
178 | results << getRssiResult(part, description)
179 | }
180 | else if (part.startsWith('lqi:')) {
181 | results << getLqiResult(part, description)
182 | }
183 | }
184 |
185 | results
186 | }
187 |
188 | private List parseContactMessage(String description) {
189 | def results = []
190 | def parts = description.split(',')
191 | parts.each { part ->
192 | part = part.trim()
193 | if (part.startsWith('contactState:')) {
194 | results << getContactResult(part, description)
195 | }
196 | else if (part.startsWith('accelerationState:')) {
197 | results << getAccelerationResult(part, description)
198 | }
199 | else if (part.startsWith('temp:')) {
200 | results << getTempResult(part, description)
201 | }
202 | else if (part.startsWith('battery:')) {
203 | results << getBatteryResult(part, description)
204 | }
205 | else if (part.startsWith('rssi:')) {
206 | results << getRssiResult(part, description)
207 | }
208 | else if (part.startsWith('lqi:')) {
209 | results << getLqiResult(part, description)
210 | }
211 | }
212 |
213 | results
214 | }
215 |
216 | private List parseOrientationMessage(String description) {
217 | def results = []
218 | def xyzResults = [x: 0, y: 0, z: 0]
219 | def parts = description.split(',')
220 | parts.each { part ->
221 | part = part.trim()
222 | if (part.startsWith('x:')) {
223 | def unsignedX = part.split(":")[1].trim().toInteger()
224 | def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
225 | xyzResults.x = signedX
226 | }
227 | else if (part.startsWith('y:')) {
228 | def unsignedY = part.split(":")[1].trim().toInteger()
229 | def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
230 | xyzResults.y = signedY
231 | }
232 | else if (part.startsWith('z:')) {
233 | def unsignedZ = part.split(":")[1].trim().toInteger()
234 | def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
235 | xyzResults.z = signedZ
236 | }
237 | else if (part.startsWith('rssi:')) {
238 | results << getRssiResult(part, description)
239 | }
240 | else if (part.startsWith('lqi:')) {
241 | results << getLqiResult(part, description)
242 | }
243 | }
244 |
245 | results << getXyzResult(xyzResults, description)
246 |
247 | results
248 | }
249 |
250 | private List parseRssiLqiMessage(String description) {
251 | def results = []
252 | // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
253 | def parts = description.split(',')
254 | parts.each { part ->
255 | part = part.trim()
256 | if (part.startsWith('lastHopRssi:')) {
257 | results << getRssiResult(part, description, true)
258 | }
259 | else if (part.startsWith('lastHopLqi:')) {
260 | results << getLqiResult(part, description, true)
261 | }
262 | else if (part.startsWith('rssi:')) {
263 | results << getRssiResult(part, description)
264 | }
265 | else if (part.startsWith('lqi:')) {
266 | results << getLqiResult(part, description)
267 | }
268 | }
269 |
270 | results
271 | }
272 |
273 | private getContactResult(part, description) {
274 | def name = "contact"
275 | def value = part.endsWith("1") ? "open" : "closed"
276 | def handlerName = value == 'open' ? 'opened' : value
277 | def linkText = getLinkText(device)
278 | def descriptionText = "$linkText was $handlerName"
279 | def isStateChange = isStateChange(device, name, value)
280 |
281 | [
282 | name: name,
283 | value: value,
284 | unit: null,
285 | linkText: linkText,
286 | descriptionText: descriptionText,
287 | handlerName: handlerName,
288 | isStateChange: isStateChange,
289 | displayed: displayed(description, isStateChange)
290 | ]
291 | }
292 |
293 | private getAccelerationResult(part, description) {
294 | def name = "acceleration"
295 | def value = part.endsWith("1") ? "active" : "inactive"
296 | def linkText = getLinkText(device)
297 | def descriptionText = "$linkText was $value"
298 | def isStateChange = isStateChange(device, name, value)
299 |
300 | [
301 | name: name,
302 | value: value,
303 | unit: null,
304 | linkText: linkText,
305 | descriptionText: descriptionText,
306 | handlerName: value,
307 | isStateChange: isStateChange,
308 | displayed: displayed(description, isStateChange)
309 | ]
310 | }
311 |
312 | private getTempResult(part, description) {
313 | def name = "temperature"
314 | def temperatureScale = getTemperatureScale()
315 | // temp is mV get celcuis is mv / 10 then convert to F
316 | //def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
317 | def value = part.split(":")[1].toDouble() / 10 // Celcius
318 | if (temperatureScale == "F")
319 | { value = value * 1.8 + 32 }
320 | log.debug TempAdjust
321 | value = (value.toDouble() + TempAdjust.toDouble()).round().toString() // PS changed to +/- temp adjustment
322 | log.debug value
323 | def linkText = getLinkText(device)
324 | def descriptionText = "$linkText was $value°$temperatureScale"
325 | def isStateChange = isTemperatureStateChange(device, name, value)
326 |
327 | storeData("temperature", value)
328 |
329 | [
330 | name: name,
331 | value: value,
332 | unit: temperatureScale,
333 | linkText: linkText,
334 | descriptionText: descriptionText,
335 | handlerName: name,
336 | isStateChange: isStateChange,
337 | displayed: displayed(description, isStateChange)
338 | ]
339 | }
340 |
341 | private getXyzResult(results, description) {
342 | def name = "threeAxis"
343 | def value = "${results.x},${results.y},${results.z}"
344 | def linkText = getLinkText(device)
345 | def descriptionText = "$linkText was $value"
346 | def isStateChange = isStateChange(device, name, value)
347 |
348 | [
349 | name: name,
350 | value: value,
351 | unit: null,
352 | linkText: linkText,
353 | descriptionText: descriptionText,
354 | handlerName: name,
355 | isStateChange: isStateChange,
356 | displayed: false
357 | ]
358 | }
359 |
360 | private getBatteryResult(part, description) {
361 | def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
362 | def name = "battery"
363 | def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
364 | def unit = "%"
365 | def linkText = getLinkText(device)
366 | def descriptionText = "$linkText Battery was ${value}${unit}"
367 | def isStateChange = isStateChange(device, name, value)
368 |
369 | [
370 | name: name,
371 | value: value,
372 | unit: unit,
373 | linkText: linkText,
374 | descriptionText: descriptionText,
375 | handlerName: name,
376 | isStateChange: isStateChange,
377 | displayed: false
378 | ]
379 | }
380 |
381 | private getRssiResult(part, description, lastHop=false) {
382 | def name = lastHop ? "lastHopRssi" : "rssi"
383 | def valueString = part.split(":")[1].trim()
384 | def value = (Integer.parseInt(valueString) - 128).toString()
385 | def linkText = getLinkText(device)
386 | def descriptionText = "$linkText was $value dBm"
387 | if (lastHop) {
388 | descriptionText += " on the last hop"
389 | }
390 | def isStateChange = isStateChange(device, name, value)
391 |
392 | [
393 | name: name,
394 | value: value,
395 | unit: "dBm",
396 | linkText: linkText,
397 | descriptionText: descriptionText,
398 | handlerName: null,
399 | isStateChange: isStateChange,
400 | displayed: false
401 | ]
402 | }
403 |
404 | /**
405 | * Use LQI (Link Quality Indicator) as a measure of signal strength. The values
406 | * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
407 | * strength. Return as a percentage of 255.
408 | *
409 | * Note: To make the signal strength indicator more accurate, we could combine
410 | * LQI with RSSI.
411 | */
412 | private getLqiResult(part, description, lastHop=false) {
413 | def name = lastHop ? "lastHopLqi" : "lqi"
414 | def valueString = part.split(":")[1].trim()
415 | def percentageOf = 255
416 | def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString()
417 | def unit = "%"
418 | def linkText = getLinkText(device)
419 | def descriptionText = "$linkText Signal (LQI) was: ${value}${unit}"
420 | if (lastHop) {
421 | descriptionText += " on the last hop"
422 | }
423 | def isStateChange = isStateChange(device, name, value)
424 |
425 | [
426 | name: name,
427 | value: value,
428 | unit: unit,
429 | linkText: linkText,
430 | descriptionText: descriptionText,
431 | handlerName: null,
432 | isStateChange: isStateChange,
433 | displayed: false
434 | ]
435 | }
436 |
437 | private Boolean isAccelerationMessage(String description) {
438 | // "acceleration: 1, rssi: 91, lqi: 255"
439 | description ==~ /acceleration:.*rssi:.*lqi:.*/
440 | }
441 |
442 | private Boolean isContactMessage(String description) {
443 | // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255"
444 | description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/
445 | }
446 |
447 | private Boolean isRssiLqiMessage(String description) {
448 | // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255"
449 | description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/
450 | }
451 |
452 | private Boolean isOrientationMessage(String description) {
453 | // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255"
454 | description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/
455 | }
456 |
457 | private String parseName(String description) {
458 | if (isSupportedDescription(description)) {
459 | return "contact"
460 | }
461 | null
462 | }
463 |
464 | private String parseValue(String description) {
465 | if (!isSupportedDescription(description)) {
466 | return description
467 | }
468 | else if (zigbee.translateStatusZoneType19(description)) {
469 | return "open"
470 | }
471 | else {
472 | return "closed"
473 | }
474 | }
475 |
476 | private parseDescriptionText(String linkText, String value, String description) {
477 | if (!isSupportedDescription(description)) {
478 | return value
479 | }
480 |
481 | value ? "$linkText was ${value == 'open' ? 'opened' : value}" : ""
482 | }
483 |
484 | def getVisualizationData(attribute) {
485 | log.debug "getChartData for $attribute"
486 | def keyBase = "measure.${attribute}"
487 | log.debug "getChartData state = $state"
488 |
489 | def dateBuckets = state[keyBase]
490 |
491 | //convert to the right format
492 | def results = dateBuckets?.sort{it.key}.collect {[
493 | date: Date.parse("yyyy-MM-dd", it.key),
494 | average: it.value.average,
495 | min: it.value.min,
496 | max: it.value.max
497 | ]}
498 |
499 | log.debug "getChartData results = $results"
500 | results
501 | }
502 |
503 | private getKeyFromDate(date = new Date()){
504 | date.format("yyyy-MM-dd")
505 | }
506 |
507 | private storeData(attribute, value) {
508 | log.debug "storeData initial state: $state"
509 | def keyBase = "measure.${attribute}"
510 | def numberValue = value.toBigDecimal()
511 |
512 | // create bucket if it doesn't exist
513 | if(!state[keyBase]) {
514 | state[keyBase] = [:]
515 | log.debug "storeData - attribute not found. New state: $state"
516 | }
517 |
518 | def dateString = getKeyFromDate()
519 | if(!state[keyBase][dateString]) {
520 | //no date bucket yet, fill with initial values
521 | state[keyBase][dateString] = [:]
522 | state[keyBase][dateString].average = numberValue
523 | state[keyBase][dateString].runningSum = numberValue
524 | state[keyBase][dateString].runningCount = 1
525 | state[keyBase][dateString].min = numberValue
526 | state[keyBase][dateString].max = numberValue
527 |
528 | log.debug "storeData date bucket not found. New state: $state"
529 |
530 | // remove old buckets
531 | def old = getKeyFromDate(new Date() - 10)
532 | state[keyBase].findAll { it.key < old }.collect { it.key }.each { state[keyBase].remove(it) }
533 | } else {
534 | //re-calculate average/min/max for this bucket
535 | state[keyBase][dateString].runningSum = (state[keyBase][dateString].runningSum.toBigDecimal()) + numberValue
536 | state[keyBase][dateString].runningCount = state[keyBase][dateString].runningCount.toInteger() + 1
537 | state[keyBase][dateString].average = state[keyBase][dateString].runningSum.toBigDecimal() / state[keyBase][dateString].runningCount.toInteger()
538 |
539 | log.debug "storeData after average calculations. New state: $state"
540 |
541 | if(state[keyBase][dateString].min == null) {
542 | state[keyBase][dateString].min = numberValue
543 | } else if (numberValue < state[keyBase][dateString].min.toBigDecimal()) {
544 | state[keyBase][dateString].min = numberValue
545 | }
546 | if(state[keyBase][dateString].max == null) {
547 | state[keyBase][dateString].max = numberValue
548 | } else if (numberValue > state[keyBase][dateString].max.toBigDecimal()) {
549 | state[keyBase][dateString].max = numberValue
550 | }
551 | }
552 | log.debug "storeData after min/max calculations. New state: $state"
553 | }
554 |
--------------------------------------------------------------------------------
/devicetypes/zenwithin/zen-thermostat-new-ui.src/zen-thermostat-new-ui.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Zen Thermostat
3 | *
4 | * Author: Zen Within
5 | * Date: 2015-02-21
6 | */
7 | metadata {
8 | definition (name: "Zen Thermostat New UI", namespace: "zenwithin", author: "ZenWithin") {
9 | capability "Actuator"
10 | capability "Thermostat"
11 | capability "Temperature Measurement"
12 | capability "Configuration"
13 | capability "Refresh"
14 | capability "Sensor"
15 | capability "Switch Level"
16 |
17 | fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019"
18 |
19 | //attribute "temperatureUnit", "number"
20 |
21 | command "setpointUp"
22 | command "setpointDown"
23 |
24 | command "setCelsius"
25 | command "setFahrenheit"
26 |
27 | // To please some of the thermostat SmartApps
28 | command "poll"
29 | }
30 |
31 | // simulator metadata
32 | simulator { }
33 |
34 | tiles (scale:2) {
35 | multiAttributeTile(name:"richtstat", type:"lighting", width:6, height:4) {
36 | tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
37 | attributeState "temperature", label: '${currentValue}', backgroundColor:"#0a1ec2"
38 | }
39 | tileAttribute("statusText", key: "SECONDARY_CONTROL") {
40 | attributeState "statusText", label: '${currentValue}'
41 | }
42 | /*
43 | tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
44 | attributeState("default", label:'${currentValue}', unit:"dF")
45 | }
46 | tileAttribute("device.temperature", key: "VALUE_CONTROL") {
47 | attributeState("default", action: "setTemperature")
48 | }
49 | tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
50 | attributeState("default", label:'${currentValue}%', unit:"%")
51 | }
52 | tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
53 | attributeState("idle", backgroundColor:"#44b621")
54 | attributeState("heating", backgroundColor:"#ffa81e")
55 | attributeState("cooling", backgroundColor:"#269bd2")
56 | }
57 | tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
58 | attributeState("off", label:'${name}')
59 | attributeState("heat", label:'${name}')
60 | attributeState("cool", label:'${name}')
61 | attributeState("auto", label:'${name}')
62 | }
63 | tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
64 | attributeState("default", label:'${currentValue}', unit:"dF")
65 | }
66 | tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
67 | attributeState("default", label:'${currentValue}', unit:"dF")
68 | }
69 |
70 | /*
71 | tileAttribute("statusText", key: "VALUE_CONTROL") {
72 | attributeState "UpandDown", label: '${currentValue}'
73 | }
74 | * Currently broken on android
75 | */
76 |
77 | /*
78 | tileAttribute("statusText", key: "SLIDER_CONTROL") {
79 | attributeState "level", action:"switch level.setLevel"
80 | }
81 | */
82 |
83 | }
84 |
85 |
86 |
87 | valueTile("frontTile", "device.temperature", width: 2, height: 2) {
88 | state("temperature", label:'${currentValue}°', backgroundColor:"#e8e3d8")
89 | }
90 |
91 | valueTile("temperature", "device.temperature", width: 2, height: 2) {
92 | state("temperature", label:'${currentValue}°', backgroundColor:"#0A1E2C")
93 | }
94 |
95 | standardTile("fanMode", "device.thermostatFanMode", decoration: "flat", width: 2, height: 2) {
96 | state "fanAuto", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-auto"
97 | state "fanOn", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-on"
98 | }
99 |
100 |
101 | standardTile("mode", "device.thermostatMode", decoration: "flat", width: 2, height: 2) {
102 | state "off", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.heating-cooling-off", nextState:"heating"
103 | state "heat", action:"setThermostatMode", backgroundColor:"#ff6e7e", icon:"st.thermostat.heat", nextState:"cooling"
104 | state "cool", action:"setThermostatMode", backgroundColor:"#90d0e8", icon:"st.thermostat.cool", nextState:"..."
105 | //state "auto", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.auto"
106 | state "heating", action:"setThermostatMode", nextState:"to_cool"
107 | state "cooling", action:"setThermostatMode", nextState:"..."
108 | state "...", action:"off", nextState:"off"
109 | }
110 |
111 | valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 2, height: 2) {
112 | state "off", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8"
113 | state "heat", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8"
114 | state "cool", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8"
115 | }
116 | valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, width: 2, height: 2) {
117 | state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
118 | }
119 | valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, width: 2, height: 2) {
120 | state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
121 | }
122 | standardTile("thermostatOperatingState", "device.thermostatOperatingState", inactiveLabel: false, width: 2, height: 2) {
123 | state "heating", backgroundColor:"#ff6e7e"
124 | state "cooling", backgroundColor:"#90d0e8"
125 | state "fan only", backgroundColor:"#e8e3d8"
126 | }
127 | standardTile("setpointUp", "device.thermostatSetpoint", decoration: "flat", width: 3, height:1) {
128 | state "setpointUp", action:"setpointUp", icon:"st.thermostat.thermostat-up", backgroundColor:"#0a1ec2"
129 | }
130 |
131 | standardTile("setpointDown", "device.thermostatSetpoint", decoration: "flat", width: 3, height:1) {
132 | state "setpointDown", action:"setpointDown", icon:"st.thermostat.thermostat-down", backgroundColor:"#0a1ec2"
133 | }
134 |
135 | standardTile("refresh", "device.temperature", decoration: "flat", width: 2, height: 2) {
136 | state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
137 | }
138 |
139 | standardTile("configure", "device.configure", decoration: "flat", width: 2, height: 2) {
140 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
141 | }
142 |
143 | main "richtstat"
144 | details(["richtstat", "setpointUp", "setpointDown", "temperature", "fanMode", "mode", "thermostatSetpoint", "refresh", "configure"])
145 | }
146 | }
147 |
148 |
149 | // parse events into attributes
150 | def parse(String description) {
151 | log.debug "Parse description $description"
152 | def map = [:]
153 | def activeSetpoint = "--"
154 | def statusTextmsg = ""
155 | statusTextmsg = "Currently ${device.currentState('thermostatOperatingState').value} and set at ${device.currentState('thermostatSetpoint').value} and the fan is in ${device.currentState('thermostatFanMode').value} mode"
156 | //statusTextmsg = "This is a static message"
157 | sendEvent("name":"statusText", "value":statusTextmsg)
158 | def tstatLevel = device.currentState('thermostatSetpoint').value
159 | sendEvent("name":"level", "value":tstatLevel)
160 | if (description?.startsWith("read attr -"))
161 | {
162 | def descMap = parseDescriptionAsMap(description)
163 | // Thermostat Cluster Attribute Read Response
164 | if (descMap.cluster == "0201" && descMap.attrId == "0000")
165 | {
166 | log.debug "LOCAL TEMPERATURE"
167 | map.name = "temperature"
168 | map.value = getTemperature(descMap.value)
169 | def receivedTemperature = map.value
170 | }
171 | else if (descMap.cluster == "0201" && descMap.attrId == "001c")
172 | {
173 | map.name = "thermostatMode"
174 | map.value = getModeMap()[descMap.value]
175 | if (map.value == "cool") {
176 | activeSetpoint = device.currentValue("coolingSetpoint")
177 | } else if (map.value == "heat") {
178 | activeSetpoint = device.currentValue("heatingSetpoint")
179 | }
180 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint)
181 | }
182 | else if (descMap.cluster == "0201" && descMap.attrId == "0011")
183 | {
184 | log.debug "COOL SET POINT"
185 | map.name = "coolingSetpoint"
186 | map.value = getTemperature(descMap.value)
187 | if (device.currentState("thermostatMode")?.value == "cool") {
188 | activeSetpoint = map.value
189 | log.debug "Active set point value: $activeSetpoint"
190 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint)
191 | }
192 | }
193 | else if (descMap.cluster == "0201" && descMap.attrId == "0012")
194 | {
195 | log.debug "HEAT SET POINT"
196 | map.name = "heatingSetpoint"
197 | map.value = getTemperature(descMap.value)
198 | if (device.currentState("thermostatMode")?.value == "heat") {
199 | activeSetpoint = map.value
200 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint)
201 | }
202 | }
203 | else if (descMap.cluster == "0201" && descMap.attrId == "0029")
204 | {
205 | log.debug "OPERATING STATE"
206 | map.name = "thermostatOperatingState"
207 | map.value = getOperatingStateMap()[descMap.value]
208 | }
209 |
210 | // Fan Control Cluster Attribute Read Response
211 | else if (descMap.cluster == "0202" && descMap.attrId == "0000")
212 | {
213 | map.name = "thermostatFanMode"
214 | map.value = getFanModeMap()[descMap.value]
215 | }
216 |
217 | }// End of Read Attribute Response
218 |
219 | /*else if (description?.startsWith("updated")) {
220 | configure()
221 | }
222 | */
223 | def result = null
224 | if (map) {
225 | result = createEvent(map)
226 | }
227 | log.debug "Parse returned $map"
228 |
229 | return result
230 | }
231 |
232 | // =============== Help Functions - Don't use log.debug in all these functins ===============
233 | def parseDescriptionAsMap(description) {
234 | (description - "read attr - ").split(",").inject([:]) { map, param ->
235 | def nameAndValue = param.split(":")
236 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
237 | }
238 | }
239 |
240 | def getModeMap() { [
241 | "00":"off",
242 | "03":"cool",
243 | "04":"heat"
244 | ]}
245 |
246 | def getOperatingStateMap() { [
247 | "0000":"idle",
248 | "0001":"heating",
249 | "0002":"cooling",
250 | "0004":"fan only",
251 | "0005":"heating",
252 | "0006":"cooling",
253 | "0008":"heating",
254 | "0009":"heating",
255 | "000A":"heating",
256 | "000D":"heating",
257 | "0010":"cooling",
258 | "0012":"cooling",
259 | "0014":"cooling",
260 | "0015":"cooling"
261 | ]}
262 |
263 | def getFanModeMap() { [
264 | "04":"fanOn",
265 | "05":"fanAuto"
266 | ]}
267 |
268 | def getTemperatureDisplayModeMap() { [
269 | "00":"C",
270 | "01":"F"
271 | ]}
272 |
273 |
274 | def getTemperature(value)
275 | {
276 | def decimalFormat = new java.text.DecimalFormat("#")
277 | def celsius = Integer.parseInt(value, 16) / 100.0 as Double
278 | def returnValue
279 |
280 | // Format to support decimal with one or two
281 | decimalFormat.setMaximumFractionDigits(2)
282 | decimalFormat.setMinimumFractionDigits(1)
283 |
284 | returnValue = decimalFormat.format(celsius);
285 |
286 | log.debug "Temperature value in C: $returnValue"
287 |
288 | if(getTemperatureScale() == "F"){
289 | returnValue = decimalFormat.format(Math.round(celsiusToFahrenheit(celsius)*10)/10.0)
290 |
291 | log.debug "Temperature value in F: $returnValue"
292 | }
293 |
294 | return returnValue
295 | }
296 |
297 |
298 |
299 | // =============== Setpoints ===============
300 | def setpointUp()
301 | {
302 | def currentMode = device.currentState("thermostatMode")?.value
303 | def currentUnit = getTemperatureScale()
304 |
305 | // check if heating or cooling setpoint needs to be changed
306 | double nextLevel = device.currentValue("thermostatSetpoint") + 1.0
307 | log.debug "Next level: $nextLevel"
308 |
309 | // check the limits
310 | if(currentUnit == "C")
311 | {
312 | if (currentMode == "cool")
313 | {
314 | if(nextLevel > 36.0)
315 | {
316 | nextLevel = 36.0
317 | }
318 | } else if (currentMode == "heat")
319 | {
320 | if(nextLevel > 32.0)
321 | {
322 | nextLevel = 32.0
323 | }
324 | }
325 | }
326 | else //in degF unit
327 | {
328 | if (currentMode == "cool")
329 | {
330 | if(nextLevel > 96.0)
331 | {
332 | nextLevel = 96.0
333 | }
334 | } else if (currentMode == "heat")
335 | {
336 | if(nextLevel > 89.0)
337 | {
338 | nextLevel = 89.0
339 | }
340 | }
341 | }
342 |
343 | log.debug "setpointUp() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}"
344 |
345 | setSetpoint(nextLevel)
346 | }
347 |
348 | def setpointDown()
349 | {
350 | def currentMode = device.currentState("thermostatMode")?.value
351 | def currentUnit = getTemperatureScale()
352 |
353 | // check if heating or cooling setpoint needs to be changed
354 | double nextLevel = device.currentValue("thermostatSetpoint") - 1.0
355 |
356 | // check the limits
357 | if (currentUnit == "C")
358 | {
359 | if (currentMode == "cool")
360 | {
361 | if(nextLevel < 8.0)
362 | {
363 | nextLevel = 8.0
364 | }
365 | } else if (currentMode == "heat")
366 | {
367 | if(nextLevel < 10.0)
368 | {
369 | nextLevel = 10.0
370 | }
371 | }
372 | }
373 | else //in degF unit
374 | {
375 | if (currentMode == "cool")
376 | {
377 | if (nextLevel < 47.0)
378 | {
379 | nextLevel = 47.0
380 | }
381 | } else if (currentMode == "heat")
382 | {
383 | if (nextLevel < 50.0)
384 | {
385 | nextLevel = 50.0
386 | }
387 | }
388 | }
389 |
390 | log.debug "setpointDown() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}"
391 |
392 | setSetpoint(nextLevel)
393 | }
394 |
395 |
396 | def setSetpoint(degrees)
397 | {
398 | def temperatureScale = getTemperatureScale()
399 | def currentMode = device.currentState("thermostatMode")?.value
400 |
401 | def degreesDouble = degrees as Double
402 | sendEvent("name":"thermostatSetpoint", "value":degreesDouble)
403 | log.debug "New set point: $degreesDouble"
404 |
405 | def celsius = (getTemperatureScale() == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1)
406 | if (currentMode == "cool") {
407 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}"
408 | }
409 | else if (currentMode == "heat") {
410 |
411 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}"
412 | }
413 | }
414 |
415 | def setHeatingSetpoint(degrees) {
416 | def temperatureScale = getTemperatureScale()
417 |
418 | def degreesDouble = degrees as Double
419 | log.debug "setHeatingSetpoint({$degreesDouble} ${temperatureScale})"
420 | sendEvent("name":"heatingSetpoint", "value":degreesDouble)
421 |
422 | def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1)
423 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}"
424 | }
425 |
426 | def setCoolingSetpoint(degrees) {
427 | def temperatureScale = getTemperatureScale()
428 |
429 | def degreesDouble = degrees as Double
430 | log.debug "setCoolingSetpoint({$degreesDouble} ${temperatureScale})"
431 | sendEvent("name":"coolingSetpoint", "value":degreesDouble)
432 |
433 | def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1)
434 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}"
435 | }
436 |
437 | // =============== Thermostat Mode ===============
438 | def modes() {
439 | ["off", "heat", "cool"]
440 | }
441 |
442 | def setThermostatMode()
443 | {
444 | def currentMode = device.currentState("thermostatMode")?.value
445 | def modeOrder = modes()
446 | def index = modeOrder.indexOf(currentMode)
447 | def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
448 | log.debug "setThermostatMode - switching from $currentMode to $next"
449 | "$next"()
450 | }
451 |
452 | def setThermostatMode(String value) {
453 | "$value"()
454 | }
455 |
456 | def off() {
457 | sendEvent("name":"thermostatMode", "value":"off")
458 | sendEvent("name":"thermostatSetpoint","value":"--")
459 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}"
460 | }
461 |
462 | def cool() {
463 | def coolingSetpoint = device.currentValue("coolingSetpoint")
464 | log.debug "Cool set point: $coolingSetpoint"
465 | sendEvent("name":"thermostatMode", "value":"cool")
466 | sendEvent("name":"thermostatSetpoint","value":coolingSetpoint)
467 | [
468 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}"
469 | ]
470 | }
471 |
472 | def heat() {
473 | def heatingSetpoint = device.currentValue("heatingSetpoint")
474 | log.debug "Heat set point: $heatingSetpoint"
475 | sendEvent("name":"thermostatMode","value":"heat")
476 | sendEvent("name":"thermostatSetpoint","value":heatingSetpoint)
477 | [
478 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}"
479 | ]
480 | }
481 |
482 | // =============== Fan Mode ===============
483 | def setThermostatFanMode()
484 | {
485 | def currentFanMode = device.currentState("thermostatFanMode")?.value
486 | def returnCommand
487 |
488 | switch (currentFanMode) {
489 | case "fanAuto":
490 | returnCommand = fanOn()
491 | break
492 | case "fanOn":
493 | returnCommand = fanAuto()
494 | break
495 | }
496 |
497 | if(!currentFanMode) {
498 | returnCommand = fanAuto()
499 | }
500 |
501 | log.debug "setThermostatFanMode - switching from $currentFanMode to $returnCommand"
502 |
503 | returnCommand
504 | }
505 |
506 | def setThermostatFanMode(String value) {
507 | "$value"()
508 | }
509 |
510 | def on() {
511 | fanOn()
512 | }
513 |
514 | def fanOn() {
515 | sendEvent("name":"thermostatFanMode", "value":"fanOn")
516 | "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
517 | }
518 |
519 | def auto() {
520 | fanAuto()
521 | }
522 |
523 | def fanAuto() {
524 | sendEvent("name":"thermostatFanMode", "value":"fanAuto")
525 | "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
526 | }
527 |
528 |
529 |
530 |
531 | // =============== SmartThings Default Fucntions: refresh, configure, poll ===============
532 | def refresh()
533 | {
534 | log.debug "refresh() - update attributes "
535 | [
536 |
537 | //Set long poll interval to 2 qs
538 | "raw 0x0020 {11 00 02 02 00 00 00}",
539 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
540 |
541 | //This is sent in this specific order to ensure that the temperature values are received after the unit/mode
542 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 800",
543 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 800",
544 |
545 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 800",
546 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 800",
547 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x29", "delay 800",
548 | "st rattr 0x${device.deviceNetworkId} 1 0x202 0", "delay 800",
549 |
550 | //Set long poll interval to 28 qs (7 seconds)
551 | "raw 0x0020 {11 00 02 1C 00 00 00}",
552 | "send 0x${device.deviceNetworkId} 1 1"
553 | ]
554 | }
555 |
556 | def poll()
557 | {
558 | refresh()
559 | }
560 |
561 | def configure()
562 | {
563 | log.debug "configure() - binding & attribute report"
564 | [
565 | //Set long poll interval to 2 qs
566 | "raw 0x0020 {11 00 02 02 00 00 00}",
567 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
568 |
569 | //Thermostat - Cluster 201
570 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
571 |
572 | "zcl global send-me-a-report 0x201 0 0x29 5 300 {3200}",
573 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
574 |
575 | "zcl global send-me-a-report 0x201 0x0011 0x29 5 300 {3200}",
576 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
577 |
578 | "zcl global send-me-a-report 0x201 0x0012 0x29 5 300 {3200}",
579 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
580 |
581 | "zcl global send-me-a-report 0x201 0x001C 0x30 5 300 {}",
582 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
583 |
584 | "zcl global send-me-a-report 0x201 0x0029 0x19 5 300 {}",
585 | "send 0x${device.deviceNetworkId} 1 1", "delay 500",
586 |
587 | //Fan Control - Cluster 202
588 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}", "delay 500",
589 |
590 | "zcl global send-me-a-report 0x202 0 0x30 5 300 {}",
591 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
592 |
593 | ] + refresh()
594 | }
595 |
596 |
597 |
598 | private hex(value) {
599 | new BigInteger(Math.round(value).toString()).toString(16)
600 | }
601 |
602 | def setLevel(value) {
603 | log.debug "Selected Level is $value"
604 | def privMin = 55
605 | def privMax = 85
606 | def newTempF = (((value.toInteger() - 1) * (privMax.toInteger() - privMin.toInteger()) / (100/1) + privMin.toInteger()))
607 | log.debug "Adjusted Range value is $newTempF"
608 | setSetpoint(newTempF)
609 | }
--------------------------------------------------------------------------------