homebridge-deconz
Version:
Homebridge plugin for deCONZ
1,288 lines (1,245 loc) • 46.1 kB
JavaScript
// homebridge-deconz/lib/Deconz/Resource.js
// Copyright © 2022-2026 Erik Baauw. All rights reserved.
//
// Homebridge plugin for deCONZ.
import { OptionParser } from 'homebridge-lib/OptionParser'
import { ApiClient } from 'hb-deconz-tools/ApiClient'
import { Deconz } from './index.js'
import { DeconzAccessory } from '../DeconzAccessory/index.js'
import '../DeconzAccessory/Gateway.js'
import { DeconzService } from '../DeconzService/index.js'
import '../DeconzService/Button.js'
const { toInstance, toInt, toObject, toString } = OptionParser
const { buttonEvent } = ApiClient
const { SINGLE, DOUBLE, LONG } = DeconzService.Button
const rtypes = ['lights', 'sensors', 'groups', 'alarmsystems']
const patterns = {
clipId: /^(S[0-9]{1,3})-([0-9a-z]{2})-([0-9a-z]{4})$/i,
swversion: /^([0-9]+)(?:\.([0-9]+)(?:\.([0-9]+)(?:_([0-9]{4}))?)?)?$/
}
// From low to high.
const sensorsPrios = [
'Power',
'Consumption',
'Temperature',
'LightLevel',
'Motion',
'Contact',
'AirPurifier',
'Thermostat',
'Flag'
]
// =============================================================================
const hueTapMap = {
34: 1002, // press 1
16: 2002, // press 2
17: 3002, // press 3
18: 4002, // press 4
100: 5002, // press 1 and 2
101: 0, // release 1 and 2
98: 6002, // press 3 and 4
99: 0 // release 3 and 4
}
const hkEvent = {
SINGLE: 0x01,
DOUBLE: 0x02,
LONG: 0x04
}
const buttonEventMap = {
0: 0, // PRESS
1: hkEvent.LONG, // HOLD
2: hkEvent.SINGLE, // SHORT_RELEASE
3: hkEvent.LONG, // LONG_RELEASE
4: hkEvent.DOUBLE, // DOUBLE_PRESS
5: hkEvent.LONG, // TRIPLE_PRESS
6: hkEvent.LONG, // QUADRUPLE_PRESS
7: hkEvent.LONG, // SHAKE
8: hkEvent.DOUBLE, // DROP
9: 0 // TILT
}
const ancillaryControlMap = {
disarmed: 1002,
already_disarmed: 1002,
armed_stay: 2002,
armed_night: 3002,
armed_away: 4002,
invalid_code: 5002,
not_ready: 0,
emergency: 6002,
fire: 7002,
panic: 8002
}
/** Delegate class for a resource on a deCONZ gateway.
*
* @memberof Deconz
*/
class Resource {
/** Parse the `uniqueid` in the resource body of a resource for a CLIP sensor.
* @param {string} uniqueid - The `uniqueid`.
* @return {object} The MultiCLIP `id`, `endpoint`, and `cluster`.
*/
static parseClipId (uniqueid) {
toString('uniqueid', uniqueid, true)
const a = patterns.clipId.exec(uniqueid.replace(/:/g, '').toUpperCase())
return {
id: a?.[1],
endpoint: a?.[2],
cluster: a?.[3]
}
}
/** Parse the `swversion` in the resource body of a resource for a Zigbee device.
* @param {string} swversion - The `swversion`.
* @return {string} The normalised version in semver format.
*/
static parseSwversion (swversion) {
if (swversion == null) {
return '0.0.0'
}
const a = patterns.swversion.exec(swversion)
if (a?.[1] === '0' && a?.[2] === '0' && a?.[3] === '0' && a?.[4] != null) {
return '0.0.' + Number(a[4]).toString()
}
return swversion
}
/** Create a new instance of a delegate of a resource.
*
* @param {DeconzAccessory.Gateway} gateway - The gateway.
* @param {string} rtype - The resource type of the resource:
* `groups`, `lights`, or `sensors`.
* @param {integer} rid - The resource ID of the resource.
* @param {object} body - The body of the resource.
*/
constructor (gateway, rtype, rid, body) {
toInstance('gateway', gateway, DeconzAccessory.Gateway)
/** The resource type of the resource: `groups`, `lights`, or `sensors`.
* @type {string}
*/
this.rtype = toString('rtype', rtype, true)
if (!(rtypes.includes(rtype))) {
throw new RangeError(`rtype: ${rtype}: not a valid resource type`)
}
/** The resource ID of the resource.
* @type {integer}
*/
this.rid = toInt('rid', rid)
/** The body of the resource.
* @type {object}
*/
this.body = toObject('body', body)
toString('body.name', body.name, true)
body.name = body.name.replace(/[^\p{L}\p{N} ']/ug, ' ')
.replace(/^[ ']*/, '')
.replace(/[ ']*$/, '')
if (rtype === 'alarmsystems') {
body.type = 'AlarmSystem'
}
toString('body.type', body.type, true)
let realDevice = false
if (
this.rtype === 'lights' ||
(this.rtype === 'sensors' && this.body.type.startsWith('Z'))
) {
const { mac, endpoint, cluster } = ApiClient.parseUniqueid(body.uniqueid)
/** The device ID.
*
* For Zigbee devices, the device ID is based on the Zigbee mac address
* of the device, from the `uniqueid` in the body of the resource.
* For virtual devices, the device ID is based on the Zigbee mac address of
* the gateway, and on the resource type and resource ID of the resource.
* The UUID of the corresponding HomeKit accessory is based on the device ID.
* @type {string}
*/
this.id = mac
if (gateway.splitdevice[this.rtype]?.[this.rid]) {
this.id += '-' + endpoint
}
/** The subtype of the corresponding HomeKit service.
*
* For Zigbee devices, the subtype is based on the Zigbee endpoint and
* cluster, from the `uniqueid` in the body of the resource.
* For virtual devices, the subtype is based on the resource type and
* resource ID.
* @type {string}
*/
this.subtype = endpoint + (cluster == null ? '' : '-' + cluster)
/** Zigbee endpoint.
* @type {string}
*/
this.endpoint = endpoint
/** Zigbee cluster
* @type {string}
*/
this.cluster = cluster
/** Zigbee device vs virtual device.
*
* Derived from the resource type and, for `sensors`, on the `type` in the
* resource body.
* @type {boolean}
*/
this.zigbee = true
realDevice = true
} else if (this.rtype === 'sensors') {
const { mac, endpoint, cluster } = ApiClient.parseUniqueid(body.uniqueid)
if (mac != null && endpoint != null && cluster != null) {
// uniqueid for proxy device has proper mac, endpoint, cluster
this.id = mac
this.subtype = endpoint + '-' + cluster
this.endpoint = endpoint
this.cluster = cluster
realDevice = true
} else {
const { id, endpoint, cluster } = Resource.parseClipId(body.uniqueid)
if (id != null && endpoint != null && cluster != null) {
// uniqueid for MultiCLIP has proper id, endpoint, cluster
this.id = gateway.id + '-' + id
this.subtype = endpoint + '-' + cluster
this.endpoint = endpoint
this.cluster = cluster
} else {
// ignore uniqueid for regular CLIP
this.subtype = rtype[0].toUpperCase() + rid
this.id = gateway.id + '-' + this.subtype
}
}
this.zigbee = false
} else {
this.subtype = rtype[0].toUpperCase() + rid
this.id = gateway.id + '-' + this.subtype
this.zigbee = false
}
/** The associated Homekit _Manufacturer_.
*
* For Zigbee devices, this is the sanitised `manufacturername` in the
* resource body.
* For virtual devices, this is the _Manufacturer_ for the gateway.
* @type {string}
*/
this.manufacturer = realDevice
? body.manufacturername.replace(/\//g, '')
: gateway.values.manufacturer
/** The associated HomeKit _Model_.
*
* For Zigbee devices, this is the sanitised `modelid` in the
* resource body.
* For virtual devices, this is the `type` in the resource body.
* @type {string}
*/
this.model = realDevice ? body.modelid : body.type
/** The associated HomeKit _Firmware Version_.
*
* For Zigbee devices, this is the sanitised `swversion` in the
* resource body.
* For virtual devices, this is the _Firmware Version_ for the gateway.
*/
this.firmware = realDevice
? Resource.parseSwversion(body.swversion)
: gateway.values.software
/** The name of the {@link DeconzService} subclass of the delegate of the
* corresponding HomeKit service, or `null` for unsupported and unknown
* resources.
*
* This is derived from the resource type and `type` in the resource body.
* @type {string}
*/
this.serviceName = this._serviceName
this.capabilities = {}
const f = 'patch' + this.serviceName
if (typeof this[f] === 'function') {
this[f](gateway)
}
}
/** The priority of the resource, when determining the primary resource for a
* device.
* @type {integer}
*/
get prio () {
if (this.rtype === 'groups') {
return -1
}
if (this.rtype === 'lights') {
return 0xFF - this.endpoint
}
return sensorsPrios.indexOf(this.serviceName)
}
/** The resource path of the resource, e.g. `/lights/1`.
*
* This is derived from the resource type and resource ID.
* @type {string}
*/
get rpath () { return '/' + this.rtype + '/' + this.rid }
get _serviceName () {
if (this.rtype === 'groups') {
return 'Light'
} else if (this.rtype === 'lights') {
switch (this.body.type) {
case 'Color dimmable light': return 'Light'
case 'Color light': return 'Light'
case 'Color temperature light': return 'Light'
case 'Dimmable light': return 'Light'
case 'Dimmable plug-in unit': return 'Light'
case 'Extended color light': return 'Light'
// case 'Consumption awareness device': return null
case 'Dimmer switch': return 'Light'
case 'Level control switch': return 'Light'
// case 'Level controllable output': return null
// case 'Door Lock': return null
// case 'Door Lock Unit': return null
case 'Fan': return 'Light'
case 'On/Off light switch': return 'Switch'
case 'On/Off light': return 'Light'
case 'On/Off output': return 'Outlet'
case 'On/Off plug-in unit': return 'Outlet'
case 'On/Off switch': return 'Switch'
case 'Smart plug': return 'Outlet'
case 'Configuration tool': return ''
case 'Range extender': return ''
case 'Warning device': return 'WarningDevice'
case 'Window covering controller': return 'WindowCovering'
case 'Window covering device': return 'WindowCovering'
default: return null
}
} else if (this.rtype === 'alarmsystems') {
return 'AlarmSystem'
} else { // (this.rtype === 'sensors')
const type = /^(CLIP|ZHA|ZGP)?([A-Za-z]*)$/.exec(this.body.type)?.[2]
switch (type) {
case 'AirPurifier': return 'AirPurifier'
case 'AirQuality': return 'AirQuality'
case 'Alarm': return 'Alarm'
case 'AncillaryControl': return 'Label'
case 'Battery': return 'Battery'
case 'CarbonMonoxide': return 'CarbonMonoxide'
case 'Consumption': return 'Consumption'
case 'DoorLock': return 'DoorLock'
case 'Daylight': return 'Daylight'
case 'DaylightOffset': return ''
case 'Fire': return 'Smoke'
case 'GenericFlag': return 'Flag'
case 'GenericStatus': return 'Status'
case 'Humidity': return 'Humidity'
case 'LightLevel': return 'LightLevel'
case 'Moisture': return 'Humidity'
case 'OpenClose': return 'Contact'
case 'ParticulateMatter': return 'AirQuality'
case 'Power': return 'Power'
case 'Presence': return 'Motion'
case 'Pressure': return 'AirPressure'
case 'RelativeRotary': return 'Label'
case 'Spectral': return ''
case 'Switch': return 'Label'
case 'Temperature': return 'Temperature'
case 'Thermostat': return 'Thermostat'
case 'Time': return ''
case 'Vibration': return 'Motion'
case 'Water': return 'Leak'
default: return null
}
}
}
/** Patch a resource corresponding to a `Light` service.
* @param {DeconzAccessory.Gateway} gateway - The gateway.
*/
patchLight (gateway) {
switch (this.manufacturer) {
case 'GLEDOPTO':
if (this.model === 'GLEDOPTO') { // Issue #244
if (
this.endpoint === '0A' &&
this.body.type === 'Dimmable light' &&
this.firmware === '1.0.2'
) {
this.model = 'RGBW'
} else if (
this.endpoint === '0B' &&
this.body.type === 'Color temperature light' &&
this.firmware === '1.3.002'
) {
this.model = 'WW/CW'
} else if (
this.endpoint === '0B' &&
this.body.type === 'Extended color light' &&
this.firmware === '1.0.2'
) {
this.model = 'RGB+CCT'
const device = gateway.deviceById[this.id]
if (device != null) {
this.model = 'RGBW'
this.capabilities.ct = false
}
} else {
return
}
gateway.vdebug('%s: set model to %j', this.rpath, this.model)
}
break
case 'LIDL Livarno Lux':
this.capabilities.ctMax = 454 // 2200 K
this.capabilities.ctMin = 153 // 6500 K
break
case '_TZE200_s8gkrkxk': // LIDL Xmas light strip
if (this.model === 'TS0601') { // Xmas light strip
this.manufacturer = 'LIDL Livarno Lux'
this.model = 'HG06467'
this.body.capabilities.bri = {
no_bri_inc: true
}
this.body.capabilities.color = {
effects: [
'steady', 'snow', 'rainbow', 'snake',
'twinkle', 'fireworks', 'flag', 'waves',
'updown', 'vintage', 'fading', 'collide',
'strobe', 'sparkles', 'carnival', 'glow'
],
effect_colors: 6,
effect_speed: true
}
}
break
default:
break
}
}
/** Patch a resource corresponding to a `Flag` service.
*/
patchFlag () {
if (this.endpoint != null && this.cluster === '0006') {
if (this.body.swversion === '0') {
this.capabilities.readonly = true
}
}
}
/** Patch a resource corresponding to a `Status` service.
*/
patchStatus () {
if (this.endpoint != null && this.cluster === '0012') {
const a = this.body.swversion.split(',')
const min = parseInt(a[0])
const max = parseInt(a[1])
if (min === 0 && max === 0) {
this.capabilities.readonly = true
} else if (min >= -127 && max <= 127 && min < max) {
this.capabilities.min = min
this.capabilities.max = max
}
}
}
/** Patch a resource corresponding to a `Label` service.
* @param {DeconzAccessory.Gateway} gateway - The gateway.
*/
patchLabel (gateway) {
// FIX_ME: use introspect until buttons and events are reported as capability
if (this.body.introspect?.buttons != null) {
this.capabilities._introspect = true
this.capabilities._buttons = {}
let dots = 0
for (const button in this.body.introspect.buttons) {
const label = this.body.introspect.buttons[button].name
this.capabilities._buttons[button] = { label, events: 0 }
if (label.includes(' Dot')) {
dots++
}
}
this.capabilities._namespace = dots === this.body.introspect.buttons.length
? gateway.Characteristics.hap.ServiceLabelNamespace.DOTS
: gateway.Characteristics.hap.ServiceLabelNamespace.ARABIC_NUMERALS
for (const value in this.body.introspect.values) {
const button = Math.round(value / 1000)
const event = value % 1000
if (this.capabilities._buttons[button] != null) {
this.capabilities._buttons[button].events |= buttonEventMap[event]
}
}
}
// End FIX_ME
const buttons = []
let dots = false
switch (this.manufacturer) {
case 'Aqara':
switch (this.model) {
case 'lumi.switch.acn047': // Aqara T2 Module
buttons.push([1, 'S1', SINGLE])
buttons.push([2, 'S2', SINGLE])
break
default:
break
}
break
case 'Bitron Home':
switch (this.model) {
case '902010/23': // Bitron remote, see #639.
dots = true
buttons.push([1, 'Dim Up', SINGLE])
buttons.push([2, 'On', SINGLE])
buttons.push([3, 'Off', SINGLE])
buttons.push([4, 'Dim Down', SINGLE])
break
default:
break
}
break
case 'Echostar':
switch (this.model) {
case 'Bell':
buttons.push([1, 'Front Doorbell', SINGLE])
buttons.push([2, 'Rear Doorbell', SINGLE])
break
default:
break
}
break
case 'EcoDim':
switch (this.model) {
case 'ED-10013':
case 'ED-10014':
case 'ED-10015':
// Sunricher 8-button remote clone, see #239.
if (this.endpoint === '01') {
buttons.push([1, 'On 1', SINGLE | LONG])
buttons.push([2, 'Off 1', SINGLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([3, 'On 2', SINGLE | LONG])
buttons.push([4, 'Off 2', SINGLE | LONG])
} else if (this.endpoint === '03') {
buttons.push([5, 'On 3', SINGLE | LONG])
buttons.push([6, 'Off 3', SINGLE | LONG])
} else if (this.endpoint === '04') {
buttons.push([7, 'On 4', SINGLE | LONG])
buttons.push([8, 'Off 4', SINGLE | LONG])
}
break
default:
break
}
break
case 'ELKO':
switch (this.model) {
case 'ElkoDimmerRemoteZHA': // ELKO ESH 316 Endevender RF, see #922.
buttons.push([1, 'Press', SINGLE])
buttons.push([2, 'Dim Up', SINGLE])
buttons.push([3, 'Dim Down', SINGLE])
break
default:
break
}
break
case 'Heiman':
switch (this.model) {
case 'RC-EF-3.0':
dots = true
buttons.push([1, 'HomeMode', SINGLE])
buttons.push([2, 'Disarm', SINGLE])
buttons.push([3, 'SOS', SINGLE])
buttons.push([4, 'Arm', SINGLE])
break
default:
break
}
break
case 'IKEA of Sweden':
switch (this.model) {
case 'Remote Control N2':
buttons.push([1, 'Dim Up', SINGLE | LONG])
buttons.push([2, 'Dim Down', SINGLE | LONG])
buttons.push([3, 'Previous', SINGLE | LONG])
buttons.push([4, 'Next', SINGLE | LONG])
break
case 'SYMFONISK Sound Controller':
if (this.cluster === '1000') {
buttons.push([1, 'Button', SINGLE | DOUBLE | LONG])
if (this.body.mode === 1) {
buttons.push([2, 'Turn Right', LONG])
buttons.push([3, 'Turn Left', LONG])
} else {
buttons.push([2, 'Turn Right', SINGLE])
buttons.push([3, 'Turn Left', SINGLE])
}
} else if (this.cluster === '0008') { // ZHARelativeRotary
// buttons.push([4, 'Turn Right', SINGLE])
// buttons.push([5, 'Turn Left', SINGLE])
}
break
case 'SYMFONISK sound remote gen2':
buttons.push([1, 'Play', SINGLE])
buttons.push([2, 'Plus', SINGLE | LONG, true])
buttons.push([3, 'Minus', SINGLE | LONG, true])
buttons.push([4, 'Previous', SINGLE])
buttons.push([5, 'Next', SINGLE])
buttons.push([6, 'One Dot', SINGLE | DOUBLE | LONG])
buttons.push([7, 'Two Dots', SINGLE | DOUBLE | LONG])
break
case 'TRADFRI remote control':
buttons.push([1, 'Power', SINGLE])
buttons.push([2, 'Dim Up', SINGLE | LONG])
buttons.push([3, 'Dim Down', SINGLE | LONG])
buttons.push([4, 'Previous', SINGLE | LONG])
buttons.push([5, 'Next', SINGLE | LONG])
break
case 'TRADFRI wireless dimmer':
if (this.body.mode === 1) {
buttons.push([1, 'Turn Right', SINGLE | LONG])
buttons.push([2, 'Turn Left', SINGLE | LONG])
} else {
buttons.push([1, 'On', SINGLE])
buttons.push([2, 'Dim Up', SINGLE])
buttons.push([3, 'Dim Down', SINGLE])
buttons.push([4, 'Off', SINGLE])
}
break
default:
break
}
break
case 'Insta':
switch (this.model) {
case 'HS_4f_GJ_1': // Gira/Jung Light Link hand transmitter
case 'WS_3f_G_1': // Gira Light Link wall transmitter
case 'WS_4f_J_1': // Jung Light Link wall transmitter
buttons.push([1, 'Off', SINGLE | DOUBLE | LONG])
buttons.push([2, 'On', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Scene 1', SINGLE])
buttons.push([4, 'Scene 2', SINGLE])
buttons.push([5, 'Scene 3', SINGLE])
buttons.push([6, 'Scene 4', SINGLE])
if (this.model !== 'WS_3f_G_1') {
buttons.push([7, 'Scene 5', SINGLE])
buttons.push([8, 'Scene 6', SINGLE])
}
break
default:
break
}
break
case 'LDS':
switch (this.model) {
case 'ZBT-DIMController-D0800':
buttons.push([1, 'Power', SINGLE])
buttons.push([2, 'Dim Up', SINGLE | LONG])
buttons.push([3, 'Dim Down', SINGLE | LONG])
buttons.push([4, 'Scene', SINGLE | LONG])
break
default:
break
}
break
case 'LIDL Livarno Lux':
switch (this.model) {
case 'HG06323':
buttons.push([1, 'On', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Dim Up', SINGLE | LONG])
buttons.push([3, 'Dim Down', SINGLE | LONG])
buttons.push([4, 'Off', SINGLE])
break
default:
break
}
break
case 'LUMI':
switch (this.model) {
case 'lumi.ctrl_neutral1':
buttons.push([1, 'Button', SINGLE])
break
case 'lumi.ctrl_neutral2':
buttons.push([1, 'Left', SINGLE])
buttons.push([2, 'Right', SINGLE])
buttons.push([3, 'Both', SINGLE])
break
case 'lumi.remote.b1acn01':
case 'lumi.remote.b186acn01':
case 'lumi.remote.b186acn02':
buttons.push([1, 'Button', SINGLE | DOUBLE | LONG])
break
case 'lumi.remote.b286acn01':
case 'lumi.remote.b286acn02':
buttons.push([1, 'Left', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Right', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Both', SINGLE | DOUBLE | LONG])
break
case 'lumi.remote.b286opcn01': // Xiaomi Aqara Opple, see #637.
case 'lumi.remote.b486opcn01': // Xiaomi Aqara Opple, see #637.
case 'lumi.remote.b686opcn01': // Xiaomi Aqara Opple, see #637.
buttons.push([1, '1', SINGLE | DOUBLE | LONG])
buttons.push([2, '2', SINGLE | DOUBLE | LONG])
if (this.model !== 'lumi.remote.b286opcn01') {
buttons.push([3, '3', SINGLE | DOUBLE | LONG])
buttons.push([4, '4', SINGLE | DOUBLE | LONG])
if (this.model === 'lumi.remote.b686opcn01') {
buttons.push([5, '5', SINGLE | DOUBLE | LONG])
buttons.push([6, '6', SINGLE | DOUBLE | LONG])
}
}
break
case 'lumi.ctrl_ln1.aq1':
case 'lumi.sensor_86sw1': // Xiaomi wall switch (single button).
case 'lumi.switch.l1aeu1': // Xiaomi Aqara H1, see #1149.
case 'lumi.switch.n1aeu1': // Xiaomi Aqara H1, see #1149.
buttons.push([1, 'Button', SINGLE | DOUBLE])
break
case 'lumi.ctrl_ln2.aq1':
case 'lumi.sensor_86sw2': // Xiaomi wall switch (two buttons).
case 'lumi.switch.l2aeu1': // Xiaomi Aqara H2, see #1149.
case 'lumi.switch.n2aeu1': // Xiaomi Aqara H2, see #1149.
buttons.push([1, 'Left', SINGLE | DOUBLE])
buttons.push([2, 'Right', SINGLE | DOUBLE])
buttons.push([3, 'Both', SINGLE | DOUBLE])
break
case 'lumi.sensor_cube':
case 'lumi.sensor_cube.aqgl01':
if (this.endpoint === '02') {
buttons.push([1, 'Side 1', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Side 2', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Side 3', SINGLE | DOUBLE | LONG])
buttons.push([4, 'Side 4', SINGLE | DOUBLE | LONG])
buttons.push([5, 'Side 5', SINGLE | DOUBLE | LONG])
buttons.push([6, 'Side 6', SINGLE | DOUBLE | LONG])
buttons.push([7, 'Cube', SINGLE | DOUBLE | LONG])
this.capabilities.toButtonEvent = (v) => {
const button = Math.floor(v / 1000)
let event = v % 1000
if (v === 7000) { // Wakeup
event = buttonEvent.SHORT_RELEASE
} else if (v === 7007 || v === 7008) { // Shake, Drop
} else if (event === 0) { // Push
event = buttonEvent.LONG_RELEASE
} else if (event === button) { // Double tap
event = buttonEvent.DOUBLE_PRESS
} else { // Flip
event = buttonEvent.SHORT_RELEASE
}
return button * 1000 + event
}
} else if (this.endpoint === '03') {
buttons.push([8, 'Turn Right', SINGLE | DOUBLE | LONG])
buttons.push([9, 'Turn Left', SINGLE | DOUBLE | LONG])
this.capabilities.toButtonEvent = (v) => {
const button = v > 0 ? 8 : 9
const event = Math.abs(v) < 4500
? buttonEvent.SHORT_RELEASE
: Math.abs(v) < 9000
? buttonEvent.DOUBLE_PRESS
: buttonEvent.LONG_RELEASE
return button * 1000 + event
}
}
break
case 'lumi.sensor_switch': // Xiaomi Mi wireless switch
// fallthrough
case 'lumi.sensor_switch.aq3': // Xiaomi Aqara smart wireless switch with gyro
buttons.push([1, 'Button', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case 'Lutron':
switch (this.model) {
case 'LZL4BWHL01 Remote': // Lutron Pico, see 102.
buttons.push([1, 'On', SINGLE])
buttons.push([2, 'Dim Up', LONG])
buttons.push([3, 'Dim Down', LONG])
buttons.push([4, 'Off', SINGLE])
break
case 'Z3-1BRL': // Lutron Aurora, see #522.
buttons.push([1, 'Button', SINGLE])
buttons.push([2, 'Turn Right', SINGLE])
buttons.push([3, 'Turn Left', SINGLE])
break
default:
break
}
break
case 'MLI':
switch (this.model) {
case 'ZBT-Remote-ALL-RGBW': // Tint remote control by Müller-Licht see deconz-rest-plugin#1209
buttons.push([1, 'Power', SINGLE])
buttons.push([2, 'Dim Up', SINGLE | LONG])
buttons.push([3, 'Dim Down', SINGLE | LONG])
buttons.push([4, 'Warm', SINGLE])
buttons.push([5, 'Cool', SINGLE])
buttons.push([6, 'Colour Wheel', SINGLE])
buttons.push([7, 'Work Light', SINGLE])
buttons.push([8, 'Sunset', SINGLE])
buttons.push([9, 'Party', SINGLE])
buttons.push([10, 'Night Light', SINGLE])
buttons.push([11, 'Campfire', SINGLE])
buttons.push([12, 'Romance', SINGLE])
break
default:
break
}
break
case 'Namron AS':
switch (this.model) {
case '4512703':
case '4512721':
case '4512772':
// Sunricher 8-button remote clone, see #239.
if (this.endpoint === '01') {
buttons.push([1, 'On 1', SINGLE | LONG])
buttons.push([2, 'Off 1', SINGLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([3, 'On 2', SINGLE | LONG])
buttons.push([4, 'Off 2', SINGLE | LONG])
} else if (this.endpoint === '03') {
buttons.push([5, 'On 3', SINGLE | LONG])
buttons.push([6, 'Off 3', SINGLE | LONG])
} else if (this.endpoint === '04') {
buttons.push([7, 'On 4', SINGLE | LONG])
buttons.push([8, 'Off 4', SINGLE | LONG])
}
break
default:
break
}
break
case 'OSRAM':
switch (this.model) {
case 'Switch 4x-LIGHTIFY':
buttons.push([1, 'Top Left', SINGLE | LONG])
buttons.push([2, 'Top Right', SINGLE | LONG])
buttons.push([3, 'Bottom Left', SINGLE | LONG])
buttons.push([4, 'Bottom Right', SINGLE | LONG])
break
default:
break
}
break
case 'Philips':
case 'Signify Netherlands B.V.':
switch (this.model) {
case 'RDM001': // Hue wall switch module
case 'RDM004': // Hue wall switch module
switch (this.body.config.devicemode) {
case 'singlerocker':
buttons.push([1, 'Rocker 1', SINGLE])
break
case 'singlepushbutton':
buttons.push([1, 'Push Button 1', SINGLE | LONG, true])
break
case 'dualrocker':
buttons.push([1, 'Rocker 1', SINGLE])
buttons.push([2, 'Rocker 2', SINGLE])
break
case 'dualpushbutton':
buttons.push([1, 'Push Button 1', SINGLE | LONG, true])
buttons.push([2, 'Push Button 2', SINGLE | LONG, true])
break
default:
break
}
break
case 'RDM002': // Hue tap dial switch
dots = true
if (this.endpoint === '01') {
buttons.push([1, '1', SINGLE | LONG, true])
buttons.push([2, '2', SINGLE | LONG, true])
buttons.push([3, '3', SINGLE | LONG, true])
buttons.push([4, '4', SINGLE | LONG, true])
} else if (this.endpoint === '14') { // ZHARelativeRotary
buttons.push([5, 'Right Turn', SINGLE])
buttons.push([6, 'Left Turn', SINGLE])
}
break
case 'ROM001': // Hue smart button
case 'RDM003': // Hue smart button
case 'RDM005': // Hue smart button v2
buttons.push([1, 'Button', SINGLE | LONG, true])
break
case 'RWL020':
case 'RWL021': // Hue dimmer switch
buttons.push([1, 'On', SINGLE | LONG])
buttons.push([2, 'Dim Up', SINGLE | LONG, true])
buttons.push([3, 'Dim Down', SINGLE | LONG, true])
buttons.push([4, 'Off', SINGLE | LONG])
break
case 'RWL022': // Hue dimmer switch (2021)
buttons.push([1, 'Power', SINGLE | LONG])
buttons.push([2, 'Dim Up', SINGLE | LONG, true])
buttons.push([3, 'Dim Down', SINGLE | LONG, true])
buttons.push([4, 'Hue', SINGLE | LONG])
break
case 'ZGPSWITCH': // Hue tap
dots = true
buttons.push([1, '1', SINGLE])
buttons.push([2, '2', SINGLE])
buttons.push([3, '3', SINGLE])
buttons.push([4, '4', SINGLE])
buttons.push([5, '1 and 2', SINGLE])
buttons.push([6, '3 and 4', SINGLE])
this.capabilities.toButtonEvent = (v) => {
return hueTapMap[v]
}
break
default:
break
}
break
case 'PhilipsFoH':
if (this.model === 'FOHSWITCH') { // Friends-of-Hue switch
buttons.push([1, 'Top Left', SINGLE | LONG])
buttons.push([2, 'Bottom Left', SINGLE | LONG])
buttons.push([3, 'Top Right', SINGLE | LONG])
buttons.push([4, 'Bottom Right', SINGLE | LONG])
buttons.push([5, 'Top Both', SINGLE | LONG])
buttons.push([6, 'Bottom Both', SINGLE | LONG])
}
break
case 'ROBB smarrt':
switch (this.model) {
case 'ROB_200-007-0':
case 'ROB_200-025-0':
// Sunricher 8-button remote clone, see #239.
if (this.endpoint === '01') {
buttons.push([1, 'On 1', SINGLE | LONG])
buttons.push([2, 'Off 1', SINGLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([3, 'On 2', SINGLE | LONG])
buttons.push([4, 'Off 2', SINGLE | LONG])
} else if (this.endpoint === '03') {
buttons.push([5, 'On 3', SINGLE | LONG])
buttons.push([6, 'Off 3', SINGLE | LONG])
} else if (this.endpoint === '04') {
buttons.push([7, 'On 4', SINGLE | LONG])
buttons.push([8, 'Off 4', SINGLE | LONG])
}
break
default:
break
}
break
case 'Schneider Electric':
if (this.model === 'FLS/AIRLINK/4' || this.model === 'FLS/SYSTEM-M/4') {
buttons.push([1, 'Top Right', SINGLE | LONG])
buttons.push([2, 'Bottom Right', SINGLE | LONG])
buttons.push([3, 'Top Left', SINGLE | LONG])
buttons.push([4, 'Bottom Left', SINGLE | LONG])
}
break
case 'Samjin':
switch (this.model) {
case 'button':
buttons.push([1, 'Button', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case 'Sunricher':
switch (this.model) {
case 'ZG2833K4_EU06': // Sunricher 4-button remote
case 'ZG2833K8_EU05': // Sunricher 8-button remote, see #529.
if (this.endpoint === '01') {
buttons.push([1, 'On 1', SINGLE | LONG])
buttons.push([2, 'Off 1', SINGLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([3, 'On 2', SINGLE | LONG])
buttons.push([4, 'Off 2', SINGLE | LONG])
} else if (this.endpoint === '03') {
buttons.push([5, 'On 3', SINGLE | LONG])
buttons.push([6, 'Off 3', SINGLE | LONG])
} else if (this.endpoint === '04') {
buttons.push([7, 'On 4', SINGLE | LONG])
buttons.push([8, 'Off 4', SINGLE | LONG])
}
break
case 'ZG2833PAC': // Sunricher C4
buttons.push([1, 'Rocker 1', SINGLE])
buttons.push([2, 'Rocker 2', SINGLE])
buttons.push([3, 'Rocker 3', SINGLE])
buttons.push([4, 'Rocker 4', SINGLE])
break
case 'ZGRC-KEY-002': // Sunricher CCT remote, see #529.
buttons.push([1, 'On', SINGLE])
buttons.push([2, 'Off', SINGLE])
buttons.push([3, 'Dim', LONG])
buttons.push([4, 'C/W', SINGLE | LONG])
break
default:
break
}
break
case 'THE LIGHT GROUP AS':
switch (this.model) {
case 'S57003':
// Sunricher 8-button remote clone, see #239.
if (this.endpoint === '01') {
buttons.push([1, 'On 1', SINGLE | LONG])
buttons.push([2, 'Off 1', SINGLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([3, 'On 2', SINGLE | LONG])
buttons.push([4, 'Off 2', SINGLE | LONG])
} else if (this.endpoint === '03') {
buttons.push([5, 'On 3', SINGLE | LONG])
buttons.push([6, 'Off 3', SINGLE | LONG])
} else if (this.endpoint === '04') {
buttons.push([7, 'On 4', SINGLE | LONG])
buttons.push([8, 'Off 4', SINGLE | LONG])
}
break
default:
break
}
break
case '_TZ3000_arfwfgoa':
switch (this.model) {
case 'TS0042': // Tuys 2-button switch, single endpoint
buttons.push([1, 'Left', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Right', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case '_TZ3000_dfgbtub0':
case '_TZ3000_i3rjdrwu':
switch (this.model) {
case 'TS0042': // Tuya 2-button switch, see #1060.
if (this.endpoint === '01') {
buttons.push([1, 'Button 1', SINGLE | DOUBLE | LONG])
} else if (this.endpoint === '02') {
buttons.push([2, 'Button 2', SINGLE | DOUBLE | LONG])
}
break
default:
break
}
break
case '_TZ3000_pzui3skt':
switch (this.model) {
case 'TS0041': // Tuya 1-button switch
buttons.push([1, 'Button', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case '_TZ3000_rrjr1q0u':
switch (this.model) {
case 'TS0043': // Tuya 3-button switch
buttons.push([1, 'Left', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Middle', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Right', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case '_TZ3000_vp6clf9d':
switch (this.model) {
case 'TS0044':
buttons.push([1, 'Bottom Left', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Bottom Right', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Top Right', SINGLE | DOUBLE | LONG])
buttons.push([4, 'Top Left', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case '_TZ3000_wkai4ga5':
switch (this.model) {
case 'TS0044':
dots = true
buttons.push([1, 'Top Left', SINGLE | DOUBLE | LONG])
buttons.push([2, 'Top Right', SINGLE | DOUBLE | LONG])
buttons.push([3, 'Bottom Left', SINGLE | DOUBLE | LONG])
buttons.push([4, 'Bottom Right', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case '_TZ3000_xabckq1v':
switch (this.model) {
case 'TS004F': // Tuya 4-button switch, single press only
buttons.push([1, 'Top Left', SINGLE])
buttons.push([2, 'Bottom Left', SINGLE])
buttons.push([3, 'Top Right', SINGLE])
buttons.push([4, 'Bottom Right', SINGLE])
break
default:
break
}
break
case 'dresden elektronik':
switch (this.model) {
case 'Kobold':
buttons.push([1, 'Button', SINGLE | LONG])
break
case 'Lighting Switch':
if (this.endpoint === '01') {
if (this.body.mode !== 2) {
gateway.vdebug(
'%s: Lighting Switch mode %d instead of 2',
this.rpath, this.body.mode
)
}
buttons.push([1, 'Top Left', SINGLE | LONG])
buttons.push([2, 'Bottom Left', SINGLE | LONG])
buttons.push([3, 'Top Right', SINGLE | LONG])
buttons.push([4, 'Bottom Right', SINGLE | LONG])
}
break
case 'Scene Switch':
buttons.push([1, 'On', SINGLE | LONG])
buttons.push([2, 'Off', SINGLE | LONG])
buttons.push([3, 'Scene 1', SINGLE])
buttons.push([4, 'Scene 2', SINGLE])
buttons.push([5, 'Scene 3', SINGLE])
buttons.push([6, 'Scene 4', SINGLE])
break
default:
break
}
break
case 'eWeLink':
switch (this.model) {
case 'WB01':
buttons.push([1, 'Press', SINGLE | DOUBLE | LONG])
break
default:
break
}
break
case 'frient AS':
switch (this.model) {
case 'KEPZB-110':
buttons.push([1, 'Disarm', SINGLE])
buttons.push([2, 'Armed Stay', SINGLE])
buttons.push([3, 'Armed Night', SINGLE])
buttons.push([4, 'Armed Away', SINGLE])
buttons.push([5, 'Invalid Code', SINGLE])
buttons.push([6, 'SOS', SINGLE])
// buttons.push([7, 'Fire', SINGLE])
// buttons.push([8, 'Panic', SINGLE])
this.capabilities.toButtonEvent = (v) => {
return ancillaryControlMap[v]
}
break
default:
break
}
break
case 'icasa':
switch (this.model) {
case 'ICZB-RM11S':
buttons.push([1, '1 Off', SINGLE | LONG])
buttons.push([2, '1 On', SINGLE | LONG])
buttons.push([3, '2 Off', SINGLE | LONG])
buttons.push([4, '2 On', SINGLE | LONG])
buttons.push([5, '3 Off', SINGLE | LONG])
buttons.push([6, '3 On', SINGLE | LONG])
buttons.push([7, '4 Off', SINGLE | LONG])
buttons.push([8, '4 On', SINGLE | LONG])
buttons.push([9, 'S1', SINGLE])
buttons.push([10, 'S2', SINGLE])
break
default:
break
}
break
case 'innr':
switch (this.model) {
case 'RC 110':
if (this.endpoint === '01') {
buttons.push([1, 'Power', SINGLE])
buttons.push([2, 'Dim Up', SINGLE | LONG])
buttons.push([3, 'Dim Down', SINGLE | LONG])
buttons.push([4, '1', SINGLE])
buttons.push([5, '2', SINGLE])
buttons.push([6, '3', SINGLE])
buttons.push([7, '4', SINGLE])
buttons.push([8, '5', SINGLE])
buttons.push([9, '6', SINGLE])
for (let i = 1; i <= 6; i++) {
const button = 7 + i * 3
buttons.push([button, `Power ${i}`, SINGLE])
buttons.push([button + 1, `Dim Up ${i}`, SINGLE | LONG])
buttons.push([button + 2, `Dim Down ${i}`, SINGLE | LONG])
}
}
break
default:
break
}
break
case 'lk':
switch (this.model) {
case 'ZBT-DIMSwitch-D0001': // Linkind 1-Key Remote Control, see #949.
buttons.push([1, 'Button', SINGLE | LONG])
this.capabilities.homekitValue = (v) => { return 1 }
break
default:
break
}
break
default:
if (this.body.type === 'ZHAAncillaryControl') {
buttons.push([1, 'Disarm', SINGLE])
buttons.push([2, 'Armed Stay', SINGLE])
buttons.push([3, 'Armed Night', SINGLE])
buttons.push([4, 'Armed Away', SINGLE])
buttons.push([5, 'Invalid Code', SINGLE])
buttons.push([6, 'Emergency', SINGLE])
buttons.push([7, 'Fire', SINGLE])
buttons.push([8, 'Panic', SINGLE])
this.capabilities.toButtonEvent = (v) => {
return ancillaryControlMap[v]
}
}
break
}
if (buttons.length > 0) {
this.capabilities.buttons = {}
for (const button of buttons) {
this.capabilities.buttons[button[0]] = {
label: button[1],
events: button[2]
}
if (button[3]) {
this.capabilities.buttons[button[0]].hasRepeat = button[3]
}
}
this.capabilities.namespace = dots
? gateway.Characteristics.hap.ServiceLabelNamespace.DOTS
: gateway.Characteristics.hap.ServiceLabelNamespace.ARABIC_NUMERALS
} else if (this.capabilities._buttons != null) {
this.capabilities.buttons = this.capabilities._buttons
this.capabilities.namespace = this.capabilities._namespace
delete this.capabilities._buttons
delete this.capabilities._namespace
}
}
/** Patch a resource corresponding to a `Motion` service.
*/
patchMotion () {
if (this.manufacturer === 'Aqara' && this.model === 'PS-S02D') {
if (this.endpoint !== '01') {
this.id += '-' + this.endpoint
}
}
}
/** Patch a resource corresponding to a `Thermostat` service.
*/
patchThermostat () {
if (
(this.manufacturer === 'Danfoss' && ['eTRV0100', 'eTRV0103'].includes(this.model)) ||
(this.manufacturer === 'ELKO' && this.model === 'Super TR') ||
(this.manufacturer === 'LUMI' && this.model === 'lumi.airrtc.agl001')
) {
this.capabilities.heatValue = 'heat'
} else {
this.capabilities.heatValue = 'auto'
}
}
/** Patch a resource corresponding to a `WindowCovering` service.
*/
patchWindowCovering () {
if (this.manufacturer === 'LUMI' && this.model === 'lumi.curtain.acn002') {
this.capabilities.maxSpeed = 2
this.capabilities.positionChange = true
} else if (this.manufacturer === 'ubisys') {
this.capabilities.useOpen = true
}
}
}
Deconz.Resource = Resource