@ream88/homebridge-nuki-latch
Version:
Nuki for latched doors
284 lines (228 loc) • 11.1 kB
JavaScript
const packageJson = require('../package.json')
const http = require('http')
let Service, Characteristic
const DOOR_STATE_DOOR_CLOSED = 2
const DOOR_STATE_DOOR_OPENED = 3
const LOCK_STATE_LOCKED = 1
const LOCK_STATE_JAMMED = 2
const LOCK_STATE_UNLOCKED = 3
const LOCK_STATE_UNLATCHED = 5
const LOCK_ACTION_UNLOCK = 1
const LOCK_ACTION_LOCK = 2
const LOCK_ACTION_UNLATCH = 3
module.exports = (homebridge) => {
Service = homebridge.hap.Service
Characteristic = homebridge.hap.Characteristic
homebridge.registerAccessory('NukiLatch', NukiLatch)
}
function readData (stream) {
return new Promise((resolve) => {
let data = ''
stream.on('data', (chunk) => {
data += chunk
})
stream.on('end', () => {
resolve(data)
})
})
}
function parseJSON (data) {
return new Promise((resolve, reject) => {
try {
resolve(JSON.parse(data))
} catch (error) {
reject(error)
}
})
}
class NukiBridge {
constructor (log, hostname, port, token) {
this.log = log
this.host = `${hostname}:${port}`
this.token = token
}
fetch (path, params = {}) {
const url = new URL(path, `http://${this.host}`)
url.searchParams.append('token', this.token)
for (const [name, value] of Object.entries(params)) {
url.searchParams.append(name, value)
}
return new Promise((resolve) => {
http.get(url.toString(), resolve)
})
}
loadDevices () {
return new Promise((resolve) => {
this.log.debug('Loading devices…')
this.fetch('/list').then(readData).then(parseJSON).then((devices) => {
this.log.debug('Devices loaded:', JSON.stringify(devices))
resolve(devices)
})
})
}
installCallback (url) {
this.log.debug('Loading callbacks…')
this.fetch('/callback/list').then(readData).then(parseJSON).then((response) => {
this.log.debug('Callbacks loaded:', response)
const callback = response.callbacks.find((callback) => callback.url === url)
if (callback) {
this.log.debug(`Callback ${url} already registered`)
} else {
this.fetch('/callback/add', { url: url }).then(readData).then(parseJSON).then(response => {
if (response.success) {
this.log.debug(`Callback ${url} successfully registered`)
} else {
this.log.error(`Callback ${url} register failed: ${response.message}`)
}
})
}
})
}
lock (id) {
this.log.debug('Locking SmartLock with ID:', id)
return this.fetch('/lockAction', { nukiId: id, action: LOCK_ACTION_LOCK })
}
unlock (id) {
this.log.debug('Unlocking SmartLock with ID:', id)
return this.fetch('/lockAction', { nukiId: id, action: LOCK_ACTION_UNLOCK })
}
unlatch (id) {
this.log.debug('Unlocking and unlatching SmartLock with ID:', id)
return this.fetch('/lockAction', { nukiId: id, action: LOCK_ACTION_UNLATCH })
}
}
class NukiLatch {
constructor (log, config) {
this.log = log
this.config = config
this.informationService = new Service.AccessoryInformation()
this.informationService
.setCharacteristic(Characteristic.Manufacturer, packageJson.author.name)
.setCharacteristic(Characteristic.Model, packageJson.name)
.setCharacteristic(Characteristic.FirmwareRevision, packageJson.version)
this.lockService = new Service.LockMechanism(config.name)
this.lockService
.getCharacteristic(Characteristic.LockTargetState)
.on('set', (state, callback) => this.setLockTargetState(state, callback))
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.lockService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.SECURED)
this.latchService = new Service.LockMechanism(this.config.name, 'latch')
this.latchService
.getCharacteristic(Characteristic.LockTargetState)
.on('set', (state, callback) => this.setLatchTargetState(state, callback))
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.latchService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.SECURED)
this.contactSensorService = new Service.ContactSensor()
this.batteryService = new Service.BatteryService()
// HTTP Server for Nuki bridge callbacks
http.createServer((request, response) => {
readData(request).then(parseJSON).then((state) => this.updateSmartLockState(state))
response.end()
}).listen(this.config.homebridgePort)
this.bridge = new NukiBridge(this.log, this.config.nukiBridgeIp, this.config.nukiBridgePort, this.config.nukiBridgeToken)
this.bridge.installCallback(`${this.config.homebridgeIp}:${this.config.homebridgePort}`)
this.bridge.loadDevices().then((devices) => {
this.door = devices.find((device) => device.nukiId === this.config.nukiId)
this.updateSmartLockState(this.door)
})
}
getServices () {
return [
this.informationService,
this.lockService,
this.latchService,
this.contactSensorService,
this.batteryService
]
}
setLockTargetState (state, callback) {
this.log.debug('setLockTargetState to:', state)
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(state)
switch (state) {
case Characteristic.LockCurrentState.SECURED:
this.bridge.lock(this.door.nukiId).then(readData).then(parseJSON).then((response) => {
if (response.success) {
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
}
})
break
case Characteristic.LockCurrentState.UNSECURED:
this.bridge.unlock(this.door.nukiId).then(readData).then(parseJSON).then((response) => {
if (response.success) {
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
}
})
break
}
callback(null)
}
setLatchTargetState (state, callback) {
this.log.debug('setLatchTargetState to:', state)
switch (state) {
case Characteristic.LockCurrentState.UNSECURED:
this.bridge.unlatch(this.door.nukiId).then(readData).then(parseJSON).then((response) => {
if (response.success) {
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
setTimeout(() => {
this.latchService.setCharacteristic(Characteristic.LockTargetState, Characteristic.LockTargetState.SECURED)
}, 3000)
}
})
break
default:
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(state)
}
callback(null)
}
updateSmartLockState (data) {
const state = data.lastKnownState ? data.lastKnownState : data
this.log.debug('updateSmartLockState:', state)
switch (state.state) {
case LOCK_STATE_LOCKED:
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.lockService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.SECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.latchService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.SECURED)
break
case LOCK_STATE_UNLOCKED:
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
this.lockService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.UNSECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.SECURED)
this.latchService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.SECURED)
break
case LOCK_STATE_UNLATCHED:
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
this.lockService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.UNSECURED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.UNSECURED)
this.latchService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.UNSECURED)
break
case LOCK_STATE_JAMMED:
this.lockService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.JAMMED)
this.lockService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.JAMMED)
this.latchService.getCharacteristic(Characteristic.LockTargetState).updateValue(Characteristic.LockTargetState.JAMMED)
this.latchService.getCharacteristic(Characteristic.LockCurrentState).updateValue(Characteristic.LockCurrentState.JAMMED)
default:
this.log.warn('Unhandled door state:', state)
this.currentState = Characteristic.LockCurrentState.UNKNOWN
}
if (state.doorsensorState === DOOR_STATE_DOOR_CLOSED) {
this.contactSensorService.getCharacteristic(Characteristic.ContactSensorState).updateValue(Characteristic.ContactSensorState.CONTACT_DETECTED)
} else if (state.doorsensorState === DOOR_STATE_DOOR_OPENED) {
this.contactSensorService.getCharacteristic(Characteristic.ContactSensorState).updateValue(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
}
if (state.batteryCritical) {
this.batteryService.getCharacteristic(Characteristic.StatusLowBattery).updateValue(Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW)
} else {
this.batteryService.getCharacteristic(Characteristic.StatusLowBattery).updateValue(Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL)
}
if (state.batteryCharging) {
this.batteryService.getCharacteristic(Characteristic.ChargingState).updateValue(Characteristic.ChargingState.CHARGING)
} else {
this.batteryService.getCharacteristic(Characteristic.ChargingState).updateValue(Characteristic.ChargingState.NOT_CHARGING)
}
this.batteryService.getCharacteristic(Characteristic.BatteryLevel).updateValue(state.batteryChargeState)
}
}