Skip to content

Commit c7e8f68

Browse files
authored
Merge pull request SmartThingsCommunity#11453 from SmartThingsCommunity/acceptance
Rolling up acceptance to production for deploy
2 parents dfba694 + 59cd2bd commit c7e8f68

File tree

9 files changed

+673
-154
lines changed

9 files changed

+673
-154
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* Ikea Button
3+
*
4+
* Copyright 2018
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+
import groovy.json.JsonOutput
18+
import physicalgraph.zigbee.zcl.DataType
19+
20+
metadata {
21+
definition (name: "Ikea Button", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", mcdSync: true) {
22+
capability "Actuator"
23+
capability "Battery"
24+
capability "Button"
25+
capability "Holdable Button"
26+
capability "Configuration"
27+
capability "Sensor"
28+
capability "Health Check"
29+
30+
fingerprint inClusters: "0000, 0001, 0003, 0009, 0B05, 1000", outClusters: "0003, 0004, 0005, 0006, 0008, 0019, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI remote control", deviceJoinName: "IKEA TRÅDFRI Remote", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_Remote_Control"
31+
fingerprint inClusters: "0000, 0001, 0003, 0009, 0102, 1000, FC7C", outClusters: "0003, 0004, 0006, 0008, 0019, 0102, 1000", manufacturer:"IKEA of Sweden", model: "TRADFRI on/off switch", deviceJoinName: "IKEA TRÅDFRI On/Off switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_On/Off_Switch"
32+
}
33+
34+
tiles {
35+
standardTile("button", "device.button", width: 2, height: 2) {
36+
state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
37+
state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC"
38+
}
39+
40+
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
41+
state "battery", label:'${currentValue}% battery', unit:""
42+
}
43+
44+
main (["button"])
45+
details(["button", "battery"])
46+
}
47+
}
48+
49+
private getCLUSTER_GROUPS() { 0x0004 }
50+
private getCLUSTER_SCENES() { 0x0005 }
51+
52+
private getREMOTE_BUTTONS() {
53+
[TOP:1,
54+
RIGHT:2,
55+
BOTTOM:3,
56+
LEFT:4,
57+
MIDDLE:5]
58+
}
59+
60+
private getONOFFSWITCH_BUTTONS() {
61+
[TOP:2,
62+
BOTTOM:1]
63+
}
64+
65+
private channelNumber(String dni) {
66+
dni.split(":")[-1] as Integer
67+
}
68+
69+
private getIkeaRemoteControlNames() {
70+
[
71+
"top button", //"Increase brightness button",
72+
"right button", //"Right button",
73+
"bottom button", //"Decrease brightness button",
74+
"left button", //"Left button",
75+
"middle button" //"On/Off button"
76+
]
77+
}
78+
private getIkeaOnOffSwitchNames() {
79+
[
80+
"bottom button", //"On button",
81+
"top button" //"Off button"
82+
]
83+
}
84+
85+
private getButtonLabel(buttonNum) {
86+
def label = "Button ${buttonNum}"
87+
88+
if (isIkeaRemoteControl()) {
89+
label = ikeaRemoteControlNames[buttonNum - 1]
90+
} else if (isIkeaOnOffSwitch()) {
91+
label = ikeaOnOffSwitchNames[buttonNum - 1]
92+
}
93+
94+
return label
95+
}
96+
97+
private getButtonName(buttonNum) {
98+
return "${device.displayName} " + getButtonLabel(buttonNum)
99+
}
100+
101+
private void createChildButtonDevices(numberOfButtons) {
102+
state.oldLabel = device.label
103+
104+
log.debug "Creating $numberOfButtons children"
105+
106+
for (i in 1..numberOfButtons) {
107+
log.debug "Creating child $i"
108+
def supportedButtons = (isIkeaRemoteControl() && i == REMOTE_BUTTONS.MIDDLE) ? ["pushed"] : ["pushed", "held"]
109+
def child = addChildDevice("Child Button", "${device.deviceNetworkId}:${i}", device.hubId,
110+
[completedSetup: true, label: getButtonName(i),
111+
isComponent: true, componentName: "button$i", componentLabel: getButtonLabel(i)])
112+
113+
child.sendEvent(name: "supportedButtonValues", value: supportedButtons.encodeAsJSON(), displayed: false)
114+
child.sendEvent(name: "numberOfButtons", value: 1, displayed: false)
115+
child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false)
116+
}
117+
}
118+
119+
def installed() {
120+
def numberOfButtons = 1
121+
122+
if (isIkeaRemoteControl()) {
123+
numberOfButtons = 5
124+
} else if (isIkeaOnOffSwitch()) {
125+
numberOfButtons = 2
126+
}
127+
128+
if (numberOfButtons > 1) {
129+
createChildButtonDevices(numberOfButtons)
130+
}
131+
132+
sendEvent(name: "supportedButtonValues", value: ["pushed", "held"].encodeAsJSON(), displayed: false)
133+
sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false)
134+
numberOfButtons.times {
135+
sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false)
136+
}
137+
138+
// These devices don't report regularly so they should only go OFFLINE when Hub is OFFLINE
139+
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
140+
}
141+
142+
def updated() {
143+
if (childDevices && device.label != state.oldLabel) {
144+
childDevices.each {
145+
def newLabel = getButtonName(channelNumber(it.deviceNetworkId))
146+
it.setLabel(newLabel)
147+
}
148+
state.oldLabel = device.label
149+
}
150+
}
151+
152+
def configure() {
153+
log.debug "Configuring device ${device.getDataValue("model")}"
154+
155+
def cmds = zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, DataType.UINT8, 30, 21600, 0x01) +
156+
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21) +
157+
zigbee.addBinding(zigbee.ONOFF_CLUSTER) +
158+
readDeviceBindingTable() // Need to read the binding table to see what group it's using
159+
160+
cmds
161+
}
162+
163+
def parse(String description) {
164+
log.debug "Parsing message from device: '$description'"
165+
def event = zigbee.getEvent(description)
166+
if (event) {
167+
log.debug "Creating event: ${event}"
168+
sendEvent(event)
169+
} else {
170+
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
171+
def descMap = zigbee.parseDescriptionAsMap(description)
172+
if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == 0x0021) {
173+
event = getBatteryEvent(zigbee.convertHexToInt(descMap.value))
174+
} else if (descMap.clusterInt == CLUSTER_SCENES ||
175+
descMap.clusterInt == zigbee.ONOFF_CLUSTER ||
176+
descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
177+
event = getButtonEvent(descMap)
178+
}
179+
}
180+
181+
def result = []
182+
if (event) {
183+
log.debug "Creating event: ${event}"
184+
result = createEvent(event)
185+
} else if (isBindingTableMessage(description)) {
186+
Integer groupAddr = getGroupAddrFromBindingTable(description)
187+
if (groupAddr != null) {
188+
List cmds = addHubToGroup(groupAddr)
189+
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
190+
} else {
191+
groupAddr = 0x0000
192+
List cmds = addHubToGroup(groupAddr) +
193+
zigbee.command(CLUSTER_GROUPS, 0x00, "${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr, 4))} 00")
194+
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
195+
}
196+
}
197+
198+
return result
199+
}
200+
}
201+
202+
private Map getBatteryEvent(value) {
203+
def result = [:]
204+
result.value = value
205+
result.name = 'battery'
206+
result.descriptionText = "${device.displayName} battery was ${result.value}%"
207+
return result
208+
}
209+
210+
private sendButtonEvent(buttonNumber, buttonState) {
211+
def child = childDevices?.find { channelNumber(it.deviceNetworkId) == buttonNumber }
212+
213+
if (child) {
214+
def descriptionText = "$child.displayName was $buttonState" // TODO: Verify if this is needed, and if capability template already has it handled
215+
216+
child?.sendEvent([name: "button", value: buttonState, data: [buttonNumber: 1], descriptionText: descriptionText, isStateChange: true])
217+
} else {
218+
log.debug "Child device $buttonNumber not found!"
219+
}
220+
}
221+
222+
private Map getButtonEvent(Map descMap) {
223+
Map ikeaRemoteControlMapping = [
224+
(zigbee.ONOFF_CLUSTER):
225+
[0x02: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.MIDDLE] }],
226+
(zigbee.LEVEL_CONTROL_CLUSTER):
227+
[0x01: { [state: "held", buttonNumber: REMOTE_BUTTONS.BOTTOM] },
228+
0x02: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.BOTTOM] },
229+
0x03: { [state: "", buttonNumber: 0] },
230+
0x04: { [state: "", buttonNumber: 0] },
231+
0x05: { [state: "held", buttonNumber: REMOTE_BUTTONS.TOP] },
232+
0x06: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.TOP] },
233+
0x07: { [state: "", buttonNumber: 0] }],
234+
(CLUSTER_SCENES):
235+
[0x07: { it == "00"
236+
? [state: "pushed", buttonNumber: REMOTE_BUTTONS.RIGHT]
237+
: [state: "pushed", buttonNumber: REMOTE_BUTTONS.LEFT] },
238+
0x08: { it == "00"
239+
? [state: "held", buttonNumber: REMOTE_BUTTONS.RIGHT]
240+
: [state: "held", buttonNumber: REMOTE_BUTTONS.LEFT] },
241+
0x09: { [state: "", buttonNumber: 0] }]
242+
]
243+
244+
def buttonState = ""
245+
def buttonNumber = 0
246+
Map result = [:]
247+
248+
if (isIkeaRemoteControl()) {
249+
Map event = ikeaRemoteControlMapping[descMap.clusterInt][descMap.commandInt](descMap.data[0])
250+
buttonState = event.state
251+
buttonNumber = event.buttonNumber
252+
} else if (isIkeaOnOffSwitch()) {
253+
if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) {
254+
buttonState = "pushed"
255+
if (descMap.commandInt == 0x00) {
256+
buttonNumber = ONOFFSWITCH_BUTTONS.BOTTOM
257+
} else if (descMap.commandInt == 0x01) {
258+
buttonNumber = ONOFFSWITCH_BUTTONS.TOP
259+
}
260+
} else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
261+
buttonState = "held"
262+
if (descMap.commandInt == 0x01) {
263+
buttonNumber = ONOFFSWITCH_BUTTONS.BOTTOM
264+
} else if (descMap.commandInt == 0x05) {
265+
buttonNumber = ONOFFSWITCH_BUTTONS.TOP
266+
}
267+
}
268+
}
269+
270+
if (buttonNumber != 0) {
271+
// Create old style
272+
def descriptionText = "${getButtonName(buttonNumber)} was $buttonState"
273+
result = [name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true]
274+
275+
// Create and send component event
276+
sendButtonEvent(buttonNumber, buttonState)
277+
}
278+
result
279+
}
280+
281+
private boolean isIkeaRemoteControl() {
282+
device.getDataValue("model") == "TRADFRI remote control"
283+
}
284+
285+
private boolean isIkeaOnOffSwitch() {
286+
device.getDataValue("model") == "TRADFRI on/off switch"
287+
}
288+
289+
private Integer getGroupAddrFromBindingTable(description) {
290+
log.info "Parsing binding table - '$description'"
291+
def btr = zigbee.parseBindingTableResponse(description)
292+
def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 }
293+
if (groupEntry != null) {
294+
log.info "Found group binding in the binding table: ${groupEntry}"
295+
Integer.parseInt(groupEntry.dstAddr, 16)
296+
} else {
297+
log.info "The binding table does not contain a group binding"
298+
null
299+
}
300+
}
301+
302+
private List addHubToGroup(Integer groupAddr) {
303+
["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}",
304+
"delay 200"]
305+
}
306+
307+
private List readDeviceBindingTable() {
308+
["zdo mgmt-bind 0x${device.deviceNetworkId} 0",
309+
"delay 200"]
310+
}

0 commit comments

Comments
 (0)