UNPKG

pimatic-hue-zll

Version:

Pimatic integration with (Zigbee Light Link based) Philips Hue

336 lines (285 loc) 12.2 kB
module.exports = (env) -> Promise = env.require 'bluebird' M = env.matcher assert = env.require 'cassert' matchTransitionExpression = (match, callback, optional=yes) -> matcher = ( (next) => next.match(" with", optional: yes) .match([" transition ", " transition time "]) .matchTimeDuration(wildcard: "{duration}", type: "text", callback) ) return if optional then match.optional(matcher) else matcher(match) class HueZLLRestorableActionHandler extends env.actions.ActionHandler hasRestoreAction: -> yes executeRestoreAction: (@simulate) => if @simulate return "would have restored the previous light state" else return @savedStateChange .then(@device.restoreLightState) .then( => "restored the previous light state" ) _saveState: (transitionTime=null) -> # Store the (promised) current light state in case we need to restore it @savedStateChange = @device.saveLightState(transitionTime) return @savedStateChange class HueZLLDimmerActionProvider extends env.actions.ActionProvider constructor: (@framework) -> parseAction: (input, context) -> # FIXME: may include non-Hue lights which don't support transitionTime dimmableLights = (device for own id, device of @framework.deviceManager.devices \ when device.hasAction("changeDimlevelTo")) return if dimmableLights?.length is 0 device = null valueTokens = null match = M(input, context) .match("dim hue ") .matchDevice(dimmableLights, (next, d) => if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous (device).""") return device = d ) .match(" to ") .matchNumericExpression( (next, ts) => valueTokens = ts ) .match('%', optional: yes) # "[with] transition [time] 5s" transitionMs = null match = matchTransitionExpression(match, ( (m, {time, unit, timeMs}) => transitionMs = timeMs ), yes) unless match? and valueTokens? then return null if valueTokens.length is 1 and not isNaN(valueTokens[0]) unless 0.0 <= parseFloat(valueTokens[0]) <= 100.0 context?.addError("Dimlevel must be between 0% and 100%") return null return { token: match.getFullMatch() nextInput: input.substring(match.getFullMatch().length) actionHandler: new HueZLLDimmerActionHandler(@framework, device, valueTokens, transitionMs) } class HueZLLDimmerActionHandler extends HueZLLRestorableActionHandler constructor: (@framework, @device, @expr, @transitionTime=null) -> assert @device? assert @expr? executeAction: (@simulate) => # First evaluate an expression into a value if needed if @expr? and isNaN(@expr) dimValue = @framework.variableManager.evaluateExpression(@expr) else dimValue = Promise.resolve @expr return Promise.join dimValue, @_saveState(@transitionTime), ( (dimlevel) => if @simulate return "would have changed dimlevel to #{dimlevel}%%" else return @device.changeDimlevelTo(dimlevel, @transitionTime) .then( => "changed dimlevel to #{dimlevel}%%" ) ) class CtActionProvider extends env.actions.ActionProvider constructor: (@framework) -> parseAction: (input, context) -> ctLights = (device for own id, device of @framework.deviceManager.devices \ when device.hasAction("changeCtTo")) return if ctLights?.length is 0 device = null valueTokens = null kelvin = false match = M(input, context) .match(["set color temperature of ", "set ct of "]) .matchDevice(ctLights, (next, d) => if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d ) .match(" to ") .matchNumericExpression( (next, ts) => valueTokens = ts ) .match(["K", " K"], optional: yes, ( => kelvin = true )) # optional "transition 5s" transitionMs = null match = matchTransitionExpression(match, (m, {time, unit, timeMs}) => transitionMs = timeMs ) unless match? and valueTokens? then return null if match? and valueTokens?.length is 1 and not isNaN(valueTokens[0]) value = parseFloat(valueTokens[0]) if kelvin and value > 0 value = 1000000 / value unless 153 <= value <= 500 context?.addError("Color temperature should be between 153 and 500 mired, or 2000 and 6535 K") return null else if valueTokens?.length > 0 value = valueTokens else return null return { token: match.getFullMatch() nextInput: input.substring(match.getFullMatch().length) actionHandler: new CtActionHandler(@framework, device, value, transitionMs) } class CtActionHandler extends HueZLLRestorableActionHandler constructor: (@framework, @device, @expr, @transitionTime=null) -> assert @device? assert @expr? executeAction: (@simulate) => # First evaluate an expression into a value if needed if @expr? and isNaN(@expr) ctValue = @framework.variableManager.evaluateExpression(@expr) else ctValue = Promise.resolve @expr return Promise.join ctValue, @_saveState(@transitionTime), ( (ct) => if @simulate return "would have changed color temperature to #{ct} mired" else return @device.changeCtTo(ct, @transitionTime) .then( => "changed color temperature to #{ct} mired" ) ) class HueSatActionProvider extends env.actions.ActionProvider constructor: (@framework) -> parseAction: (input, context) -> colorLights = (device for own id, device of @framework.deviceManager.devices \ when device.hasAction("changeHueSatTo")) return if colorLights?.length is 0 hueValueTokens = null satValueTokens = null device = null hueMatcher = (next) => next.match(" hue ") .matchNumericExpression( (next, ts) => hueValueTokens = ts ) .match('%', optional: yes) satMatcher = (next) => next.match([" sat ", " saturation "]) .matchNumericExpression( (next, ts) => satValueTokens = ts ) .match('%', optional: yes) match = M(input, context) .match("set color of ") .matchDevice(colorLights, (next, d) => if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d ) .match(" to") .or([ ( (next) => hueMatcher(next) .optional( (next) => satMatcher(next.match(" and", optional: yes)) ) ), ( (next) => satMatcher(next) .optional( (next) => hueMatcher(next.match(" and", optional: yes)) ) ) ]) # optional "transition 5s" transitionMs = null match = matchTransitionExpression(match, (m, {time, unit, timeMs}) => transitionMs = timeMs ) if not (match? and (hueValueTokens? or satValueTokens?)) return null if hueValueTokens?.length is 1 and not isNaN(hueValueTokens[0]) hueExpr = parseFloat(hueValueTokens[0]) unless hueExpr? and (0.0 <= hueExpr <= 100.0) context?.addError("Hue value should be between 0% and 100%") return null else if hueValueTokens?.length > 0 hueExpr = hueValueTokens if satValueTokens?.length is 1 and not isNaN(satValueTokens[0]) satExpr = parseFloat(satValueTokens[0]) unless satExpr? and (0.0 <= satExpr <= 100.0) context?.addError("Saturation value should be between 0% and 100%") return null else if satValueTokens?.length > 0 satExpr = satValueTokens return { token: match.getFullMatch() nextInput: input.substring(match.getFullMatch().length) actionHandler: new HueSatActionHandler(@framework, device, hueExpr, satExpr, transitionMs) } class HueSatActionHandler extends HueZLLRestorableActionHandler constructor: (@framework, @device, @hueExpr, @satExpr, @transitionTime=null) -> assert @device? assert @hueExpr? or @satExpr? executeAction: (@simulate) => # First evaluate an expression into a value if needed if @hueExpr? and isNaN(@hueExpr) huePromise = @framework.variableManager.evaluateExpression(@hueExpr) else huePromise = Promise.resolve @hueExpr if @satExpr? and isNaN(@satExpr) satPromise = @framework.variableManager.evaluateExpression(@satExpr) else satPromise = Promise.resolve @satExpr return Promise.join huePromise, satPromise, @_saveState(@transitionTime), @_changeHueSat _changeHueSat: (hueValue, satValue) => if hueValue? and satValue? f = (hue, sat) => @device.changeHueSatTo hue, sat, @transitionTime msg = "changed color to hue #{hueValue}%% and sat #{satValue}%%" else if hueValue? f = (hue, sat) => @device.changeHueTo hue, @transitionTime msg = "changed color to hue #{hueValue}%%" else if satValue? f = (hue, sat) => @device.changeSatTo sat, @transitionTime msg = "changed color to sat #{satValue}%%" msg += " transition time #{@transitionTime}ms" if @transitionTime? if @simulate return Promise.resolve "would have #{msg}" else return f(hueValue, satValue).then( => msg ) class ActivateHueSceneActionProvider extends env.actions.ActionProvider constructor: (@framework) -> parseAction: (input, context) -> hueScenesDevices = (device for own id, device of @framework.deviceManager.devices \ when device.hasAction("activateScene")) hueLightGroupDevices = (device for own id, device of @framework.deviceManager.devices \ when device.constructor.name.match(/^HueZLL.+LightGroup$/)) return if hueScenesDevices.length is 0 # For Hue scenes, at the moment we just need one (arbitrary) HueZLLScenes device device = hueScenesDevices[0] # See if we have a list of sceneNames ready sceneNames = (('"'+name+'"' for name in device.getKnownSceneNames())) or [] sceneExpr = null hueGroupDevice = null match = M(input, context) .match("activate hue scene ") .or([ ( (next) => next.match(sceneNames, (m, tokens) => sceneExpr = tokens ) ), ( (next) => next.matchStringWithVars( (m, tokens) => sceneExpr = tokens ) ) ]).optional( (next) => next.match([" on group ", " limited to group "]) .matchDevice(hueLightGroupDevices, (next, d) => hueGroupDevice = d ) ) if match?.hadMatch() and sceneExpr? and device? return { token: match.getFullMatch() nextInput: input.substring(match.getFullMatch().length) actionHandler: new ActivateHueSceneActionHandler(@framework, device, sceneExpr, hueGroupDevice) } else return null class ActivateHueSceneActionHandler extends env.actions.ActionHandler constructor: (@framework, @device, @sceneExpr, @hueGroup=null) -> executeAction: (simulate, context) -> return @framework.variableManager.evaluateStringExpression(@sceneExpr).then( (sceneName) => msg = "activated Hue scene #{sceneName}" msg += " on group #{@hueGroup.id}" if @hueGroup? if simulate return "would have #{msg}" else return @device.activateScene(sceneName, @hueGroup?.hue.hueId).then( => msg ) ) return exports = { HueZLLDimmerActionProvider, CtActionProvider, HueSatActionProvider, ActivateHueSceneActionProvider }