UNPKG

pimatic-nuki-reloaded

Version:
558 lines (479 loc) 19.6 kB
# The nuki-reloaded plugin module.exports = (env) -> Promise = env.require 'bluebird' nukiApi = require 'nuki-bridge-api' NukiObject = require 'nuki-bridge-api/lib/nuki' commons = require('pimatic-plugin-commons')(env) M = env.matcher _ = require('lodash') internalIp = require('internal-ip') # ###NukiPlugin class class NukiPlugin extends env.plugins.Plugin init: (app, @framework, @config) => @debug = @config.debug || false @bridge = new nukiApi.Bridge @config.host, @config.port, @config.token env.logger.debug "New nukiApi.Bridge created" @callbackPort = @config.callbackPort ? 12321 @restartTime = @config.restartTime ? 60000 @nrOfRestarts = 10 @bridgeReady = false @connectNuki = () => internalIp.v4() .then (ip)=> @ip = ip env.logger.debug "Ip address for callback: " + @ip return @bridge.getCallbackUrls() .then (urls)=> env.logger.debug "Current callbacks: " + JSON.stringify(urls,null,2) if _.size(urls)>=3 env.logger.info "Maximum number of callbacks reached on Nuki bridge" if @nrOfRestarts > 0 env.logger.info "Please remove a callback on the bridge. The connection to the bridge will restart in " + @restartTime/1000 + " seconds. " + @nrOfRestarts + " restarts left." @nrOfRestarts -= 1 @retryTimer = setTimeout(@connectNuki,@restartTime) else env.logger.info "Restarts unsuccesful. Please remove a callback on the bridge and restart the plugin" throw new Error("Maximum number of callbacks reached on Nuki bridge, Please remove a callback on the bridge and restart the plugin") else for cb in urls _url = @ip + ":" + @callbackPort if String(cb.url).indexOf(_url)>=0 env.logger.debug "callback found, deleting" @bridge.removeCallback(cb.id) env.logger.info "Adding callback" return @bridge.addCallback(@ip, @callbackPort, true) .then (cbs)=> #env.logger.debug "Callback added " + JSON.stringify(cbs,null,2) cbs.on 'action', @stateHandler cbs.on 'action', @batteryCriticalHandler .finally ()=> @emit 'bridgeReady' @bridgeReady = true env.logger.debug "Callback on '#{@ip}' initialized" .catch (err)=> switch err.code when 'ECONNREFUSED' env.logger.info "Nuki bridge can't be reached" if @nrOfRestarts > 0 env.logger.info "Please enable the bridge. The connection to the bridge will restart in " + @restartTime/1000 + " seconds. " + @nrOfRestarts + " restarts left." @nrOfRestarts -= 1 @retryTimer = setTimeout(@connectNuki,@restartTime) else env.logger.info "Restarts unsuccesful. Please enable the bridge and restart the plugin" else env.logger.debug "Info initializing callback: " + err @bridgeReady = false @connectNuki() @base = commons.base @, 'Plugin' # register devices deviceConfigDef = require("./device-config-schema") @framework.deviceManager.registerDeviceClass("NukiDevice", configDef: deviceConfigDef.NukiDevice, createCallback: (@config, lastState) => new NukiDevice(@config, @, lastState) ) @framework.on "after init", => # Check if the mobile-frontend was loaded and get a instance mobileFrontend = @framework.pluginManager.getPlugin 'mobile-frontend' if mobileFrontend? mobileFrontend.registerAssetFile 'js', 'pimatic-nuki-reloaded/ui/nuki.coffee' mobileFrontend.registerAssetFile 'css', 'pimatic-nuki-reloaded/ui/nuki.css' mobileFrontend.registerAssetFile 'html', 'pimatic-nuki-reloaded/ui/nuki.jade' #mobileFrontend.registerAssetFile 'js', 'pimatic-tado-reloaded/ui/vendor/spectrum.js' #mobileFrontend.registerAssetFile 'css', 'pimatic-tado-reloaded/ui/vendor/spectrum.css' #mobileFrontend.registerAssetFile 'js', 'pimatic-tado-reloaded/ui/vendor/async.js' else env.logger.warn 'your plugin could not find the mobile-frontend. No gui will be available' @framework.ruleManager.addActionProvider(new NukiActionProvider(@framework)) # auto-discovery @framework.deviceManager.on('discover', (eventData) => @framework.deviceManager.discoverMessage 'pimatic-nuki-reloaded', 'Searching for Nuki Smart Locks.' @lastId = null @bridge.list().then (nukiDevices) => for device in nukiDevices do (device) => @lastId = @base.generateDeviceId @framework, "nuki", @lastId if _.find(@framework.deviceManager.devicesConfig,(d) => #env.logger.debug "Discover---->: " +d.nukiId+ ", = "+device.nukiId return d.nukiId is device.nukiId ) env.logger.info "Device '" + device.nukiId + "' already in config" else deviceConfig = id: @lastId name: device.name class: 'NukiDevice' nukiId: String device.nukiId @framework.deviceManager.discoveredDevice( 'pimatic-nuki-reloaded', "#{deviceConfig.name} (#{deviceConfig.nukiId})", deviceConfig ) ) stateHandler: (state,response) => resp = state: state response: response #env.logger.debug "emitting action " + JSON.stringify(resp,null,2) @emit 'action', resp batteryCriticalHandler: (batteryCritical) => #env.logger.debug "emitting batteryCritical" @emit 'batteryCritical', batteryCritical class NukiDevice extends env.devices.Device template: 'nuki' actions: changeStateTo: params: state: type: "boolean" constructor: (@config, @plugin, lastState) -> @id = @config.id @name = @config.name @nukiId = @config.nukiId @ip = @plugin.ip @configInterval = @config.interval ? 21600 env.logger.debug "Start constructor Nuki device" # attributes state is used for gui button @addAttribute('state', description: "State of the lock" type: "boolean" hidden: true ) @addAttribute('lock', description: "Status of the lock" type: "string" acronym: "door" hidden: false ) @addAttribute('status', description: "Status of the communicsation and lock" type: "string" acronym: "status" ) @addAttribute('battery', description: "Critical status of the battery" type: "boolean" acronym: "battery" labels: ["critical","ok"] ) # fixed attributes @_state = laststate?.state?.value ? false @_battery = laststate?.battery?.value ? false @_lock = laststate?.lock?.value ? "" @_status = laststate?.status?.value ? "ready" #creation of extra info attributes for info in @config.infos do (info) => env.logger.debug "Info: " + JSON.stringify(info,null,2) @addAttribute(info.name , description: info.name type: info.type ? "string" acronym: info.acronym ? info.name unit: info.unit ? "" ) if info.type is "number" @[addUnderscore info.name] = laststate?[info.name]?.value ? 0 else if @attributes[info.name].type is "boolean" @[addUnderscore info.name] = laststate?[info.name]?.value ? false else @[addUnderscore info.name] = laststate?[info.name]?.value ? "" env.logger.debug "@_[info.name] " + @[addUnderscore info.name] @_createGetter(info.name, => return Promise.resolve @[addUnderscore info.name] ) #initialization on restart of plugin/pimatic @plugin.on 'bridgeReady', @bridgeReadyHandler = ()=> env.logger.debug "BridgeReady received for " + @id @nuki = new NukiObject @plugin.bridge, @config.nukiId env.logger.debug "@nuki created for " + @id @plugin.on 'action', @stateHandler env.logger.debug "State handler created for " + @id @plugin.on 'batteryCritical', @batteryCriticalHandler env.logger.debug "BatteryCritical handler created for " + @id @_requestUpdate() #initialization on restart of device if @plugin.bridgeReady @nuki = new NukiObject @plugin.bridge, @config.nukiId env.logger.debug "@nuki created for " + @id @plugin.on 'action', @stateHandler env.logger.debug "State handler created for " + @id @plugin.on 'batteryCritical', @batteryCriticalHandler env.logger.debug "BatteryCritical handler created for " + @id @_requestUpdate() super() getTemplateName: -> "nuki" addUnderscore = (name)-> return "_" + name batteryCriticalHandler: (battery, nukiId)=> unless nukiId is @nukiId then return @_setBattery(battery ? true) stateHandler: (_action)=> unless (Number _action.response.nukiId) == Number(@nukiId) then return state = _action.state lastKnownState = _action.response?.lastKnownState ? _action.response ### LockStateV1_2 = UNCALIBRATED: 0, LOCKED: 1, UNLOCKING: 2, UNLOCKED: 3, LOCKING: 4, UNLATCHED: 5, UNLOCKED_LOCK_N_GO: 6, UNLATCHING: 7, MOTOR_BLOCKED: 254, UNDEFINED: 255 ### env.logger.debug "StateHandler, state received: " + state env.logger.debug "StateHandler, lastKnownState received: " + JSON.stringify(lastKnownState,null,2) switch state when nukiApi.lockState.LOCKED env.logger.debug "State LOCKED received for '#{@id}'" @_setState on @_setLock 'locked' @_setStatus 'received locked' when nukiApi.lockState.LOCKING env.logger.debug "State LOCKING received for '#{@id}'" @_setState on @_setLock 'locking' @_setStatus 'received locking' when nukiApi.lockState.UNLOCKING env.logger.debug "State UNLOCKING received for '#{@id}'" @_setState off @_setLock 'unlocking' @_setStatus 'received unlocking' when nukiApi.lockState.UNLOCKED env.logger.debug "State UNLOCKED received for '#{@id}'" @_setState off @_setLock 'unlocked' @_setStatus 'received unlocked' when nukiApi.lockState.UNLATCH env.logger.debug "State UNLATCH received for '#{@id}'" @_setState off @_setLock 'open' @_setStatus 'received open' when nukiApi.lockState.LOCK_N_GO env.logger.debug "State LOCK_N_GO received for '#{@id}'" @_setLock 'lock-n-go' @_setStatus 'received lock-n-go' when nukiApi.lockState.MOTOR_BLOCKED env.logger.debug "State MOTOR_BLOCKED received for '#{@id}'" @_setLock 'motor blocked' @_setStatus 'received motor blocked', false when nukiApi.lockState.UNDEFINED env.logger.debug "State UNDEFINED received for '#{@id}'" @_setLock 'undefined' @_setStatus 'received undefined', false else env.logger.debug "Unknown State received for '#{@id}', State nr: " + state @_setLock 'unknown' @_setStatus "received unknown: #{state}", false if lastKnownState?.batteryCritical? @_setBattery lastKnownState.batteryCritical #check if extra info values are received for info in @config.infos if lastKnownState?[info.name]? switch info.type when "number" @[addUnderscore info.name] = Number lastKnownState[info.name] when "boolean" @[addUnderscore info.name] = Boolean lastKnownState[info.name] else @[addUnderscore info.name] = lastKnownState[info.name] @emit info.name, @[addUnderscore info.name] actionHandler: (action)=> ### LockAction = UNLOCK: 1, LOCK: 2, UNLATCH: 3, LOCK_N_GO: 4, LOCK_N_GO_WITH_UNLATCH: 5 ### env.logger.debug "ActionHandler, action received: " + JSON.stringify(action,null,2) switch action when nukiApi.lockAction.LOCK @nuki.lockAction(nukiApi.lockAction.LOCK, false) #nowait .then (resp)=> env.logger.debug "'#{@id}' locked" @_setState on @_setLock "locked" .catch (err) => @_setStatus "set locked error " + err.statusCode env.logger.debug "Error locking '#{@id}': " + JSON.stringify(err,null,2) when nukiApi.lockAction.UNLOCK @nuki.lockAction(nukiApi.lockAction.UNLOCK, false) #nowait .then (resp)=> env.logger.debug "'#{@id}' unlocked" @_setState off @_setLock "unlocked" .catch (err) => env.logger.debug "Error unlocking '#{@id}': " + JSON.stringify(err,null,2) else env.logger.debug "Action '#{action}' not implemented" Promise.resolve() _requestUpdate: () => env.logger.debug "Requesting update" @_setStatus "requesting update" @plugin.bridge.list() .then (list) => _nuki = _.find(list,(n)=>(Number n.nukiId) == (Number @nukiId)) if _nuki?.lastKnownState? _state = _nuki.lastKnownState.state env.logger.debug "LockState is #{_state}" if typeof _state is "string" _state = parseInt _state @stateHandler {state: _state, response: _nuki} .catch (error) => @_setStatus "requesting update error: " + error.statusCode, false env.logger.error "Error requesting update: " + (error.code ? error) .finally () => @scheduleUpdate = setTimeout(@_requestUpdate, @configInterval * 1000) changeStateTo: (state) -> if state is @_state then return if Boolean state @_setStatus "set locked" @actionHandler(nukiApi.lockAction.LOCK) .then ()=> @_setLock "locked" @_setState state return Promise.resolve() .catch (err)=> env.logger.debug "Error " + err @_setStatus "set locked error: " + err.statusCode, false return Promise.reject() else @_setStatus "set unlocked" @actionHandler(nukiApi.lockAction.UNLOCK) .then ()=> @_setLock "unlocked" @_setState state return Promise.resolve() .catch (err)=> env.logger.debug "Error " + err @_setStatus "set unlocked error: " + err.statusCode, false return Promise.reject() getState: () -> Promise.resolve @_state _setState: (state) => @_state = state @emit 'state', state getBattery: () -> Promise.resolve @_battery _setBattery: (battery) => @_battery = battery @emit 'battery', battery getLock: () -> Promise.resolve @_lock _setLock: (status) => @_lock = status @emit 'lock', status getStatus: () -> Promise.resolve @_status _setStatus: (status, reset=true) => @_status = status @emit 'status', status # clear running readyTimer clearTimeout(@readyTimer) if @readyTimer? # reset status if reset @readyTimer = setTimeout(=> @_status = "ready" @emit "status", "ready" ,3000) execute: (command, options) => return new Promise((resolve,reject) => env.logger.debug "Execute command: " + command + ", options: " + JSON.stringify(options,null,2) switch command when "lock" @_setStatus "set locked" env.logger.debug "Lock Nuki #{@id}" @nuki.lockAction(nukiApi.lockAction.LOCK, false) #nowait .then (resp)=> env.logger.debug "Nuki locked" @_setState on @_setLock "locked" resolve() .catch (err) => env.logger.debug "Error locking #{@id}: " + JSON.stringify(err,null,2) @_setStatus "set locked error "+err.statusCode, false reject() when "unlock" env.logger.debug "Unlock Nuki #{@id}" @_setStatus "set unlocked" @nuki.lockAction(nukiApi.lockAction.UNLOCK, false) #nowait .then (resp)=> env.logger.debug "Nuki #{@id} unlocked" @_setState off @_setLock "unlocked" resolve() .catch (err) => env.logger.debug "Error unlocking #{@id}: " + JSON.stringify(err,null,2) @_setStatus "set unlocked error "+err.statusCode, false reject() else env.logger.debug "Command not implemented: " + command reject() resolve() ) destroy: () -> clearTimeout(@scheduleUpdate) @plugin.removeListener 'action', @stateHandler @plugin.removeListener 'batteryCritical', @batteryCriticalHandler @plugin.removeListener 'bridgeReady', @bridgeReadyHandler super() class NukiActionProvider extends env.actions.ActionProvider constructor: (@framework) -> parseAction: (input, context) => nukiDevice = null @options = {} nukiDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.config.class == "NukiDevice" ).value() setCommand = (command) => @command = command m = M(input, context) .match('nuki ') .matchDevice(nukiDevices, (m, d) -> # Already had a match with another device? if nukiDevice? and nukiDevices.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return nukiDevice = d ) .or([ ((m) => return m.match(' lock', (m) => setCommand('lock') match = m.getFullMatch() ) ), ((m) => return m.match(' unlock', (m) => setCommand('unlock') match = m.getFullMatch() ) ) ]) match = m.getFullMatch() if match? #m.hadMatch() env.logger.debug "Rule matched: '", match, "' and passed to Action handler" return { token: match nextInput: input.substring(match.length) actionHandler: new NukiActionHandler(@framework, nukiDevice, @command, @options) } else return null class NukiActionHandler extends env.actions.ActionHandler constructor: (@framework, @nukiDevice, @command, @options) -> executeAction: (simulate) => if simulate return __("would have Nuki action \"%s\"", "") else @nukiDevice.execute(@command, @options) .then(()=> return __("\"%s\" Rule executed", @command) ).catch((err)=> return __("\"%s\" Rule not executed", "") ) # ###Finally # Create a instance of NukiPlugin # and return it to the framework. return new NukiPlugin