UNPKG

pimatic

Version:

A home automation server and framework for the Raspberry PI running on node.js

1,555 lines (1,277 loc) 45.1 kB
### Action Provider ================= A Action Provider can parse a action of a rule string and returns an Action Handler for that. The Action Handler offers a `executeAction` method to execute the action. For actions and rule explanations take a look at the [rules file](rules.html). ### __ = require("i18n-pimatic").__ Promise = require 'bluebird' assert = require 'cassert' _ = require('lodash') S = require('string') M = require './matcher' module.exports = (env) -> ### The ActionProvider ---------------- The base class for all Action Providers. If you want to provide actions in your plugin then you should create a sub class that implements the `parseAction` function. ### class ActionProvider # ### parseAction() ### This function should parse the given input string `input` and return an ActionHandler if handled by the input of described action, otherwise it should return `null`. ### parseAction: (input, context) => throw new Error("Your ActionProvider must implement parseAction") ### The Action Handler ---------------- The base class for all Action Handler. If you want to provide actions in your plugin then you should create a sub class that implements a `executeAction` function. ### class ActionHandler extends require('events').EventEmitter # ### executeAction() ### Ìt should return a promise that gets fulfilled with describing string, that explains what was done or would be done. If `simulate` is `true` the Action Handler should not execute the action. It should just return a promise fulfilled with a descriptive string like "would _..._". Take a look at the Log Action Handler for a simple example. ### executeAction: (simulate) => throw new Error("Should be implemented by a subclass") hasRestoreAction: => no setup: -> # You must overwrite this method and set up your listener here. # You should call super() after that. if @_setupCalled then throw new Error("Setup already called!") @_setupCalled = yes destroy: -> # You must overwrite this method and remove your listener here. # You should call super() after that. unless @_setupCalled then throw new Error("Destroy called, but setup was not called!") delete @_setupCalled @emit "destroy" @removeAllListeners() executeRestoreAction: (simulate) => throw new Error( "executeRestoreAction must be implemented when hasRestoreAction returns true" ) dependOnDevice: (device) -> recreateEmitter = (=> @emit "recreate") device.on "changed", recreateEmitter device.on "destroyed", recreateEmitter @on 'destroy', => device.removeListener "changed", recreateEmitter device.removeListener "destroyed", recreateEmitter dependOnVariable: (variableManager, varName) -> recreateEmitter = ( (variable) => if variable.name isnt varName return @emit "recreate" ) variableManager.on "variableRemoved", recreateEmitter @on 'destroy', => variableManager.removeListener "variableRemoved", recreateEmitter ### The Log Action Provider ------------- Provides log action, so that rules can use `log "some string"` in the actions part. It just prints the given string to the logger. ### class LogActionProvider extends ActionProvider constructor: (@framework) -> parseAction: (input, context) -> stringToLogTokens = null fullMatch = no setLogString = (m, tokens) => stringToLogTokens = tokens m = M(input, context) .match("log ") .matchStringWithVars(setLogString) if m.hadMatch() match = m.getFullMatch() return { token: match nextInput: input.substring(match.length) actionHandler: new LogActionHandler(@framework, stringToLogTokens) } else return null class LogActionHandler extends ActionHandler constructor: (@framework, @stringToLogTokens) -> executeAction: (simulate, context) -> @framework.variableManager.evaluateStringExpression(@stringToLogTokens).then( (strToLog) => if simulate # just return a promise fulfilled with a description about what we would do. return __("would log \"%s\"", strToLog) else # else we should log the string. # But we don't do this because the framework logs the description anyway. So we would # doubly log it. #env.logger.info stringToLog return strToLog ) ### The SetVariable ActionProvider ------------- Provides log action, so that rules can use `log "some string"` in the actions part. It just prints the given string to the logger. ### class SetVariableActionProvider extends ActionProvider constructor: (@framework) -> parseAction: (input, context) -> result = null varsAndFunsWriteable = @framework.variableManager.getVariablesAndFunctions(readonly: false) M(input, context) .match("set ", optional: yes) .matchVariable(varsAndFunsWriteable, (next, variableName) => next.match([" to ", " := ", " = "], (next) => next.or([ ( (next) => return next.matchNumericExpression( (next, rightTokens) => match = next.getFullMatch() variableName = variableName.substring(1) result = { variableName, rightTokens, match } ) ), ( (next) => return next.matchStringWithVars( (next, rightTokens) => match = next.getFullMatch() variableName = variableName.substring(1) result = { variableName, rightTokens, match } ) ) ]) ) ) if result? variables = @framework.variableManager.extractVariables(result.rightTokens) unless @framework.variableManager.isVariableDefined(result.variableName) context.addError("Variable $#{result.variableName} is not defined.") return null for v in variables? unless @framework.variableManager.isVariableDefined(v) context.addError("Variable $#{v} is not defined.") return null return { token: result.match nextInput: input.substring(result.match.length) actionHandler: new SetVariableActionHandler( @framework, result.variableName, result.rightTokens ) } else return null class SetVariableActionHandler extends ActionHandler constructor: (@framework, @variableName, @rightTokens) -> setup: -> @dependOnVariable(@framework.variableManager, @variableName) super() executeAction: (simulate, context) -> if simulate # just return a promise fulfilled with a description about what we would do. return Promise.resolve __("would set $%s to value of %s", @variableName, _(@rightTokens).reduce( (left, right) => "#{left} #{right}" ) ) else return @framework.variableManager.evaluateExpression(@rightTokens).then( (value) => @framework.variableManager.setVariableToValue(@variableName, value) return Promise.resolve("set $#{@variableName} to #{value}") ) ### The SetPresence ActionProvider ------------- Provides set presence action, so that rules can use `set presence of <device> to present|absent` in the actions part. ### class SetPresenceActionProvider extends ActionProvider constructor: (@framework) -> parseAction: (input, context) -> retVar = null presenceDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("changePresenceTo") ).value() device = null state = null match = null m = M(input, context).match(['set presence of ']) m.matchDevice(presenceDevices, (m, d) -> m.match([' present', ' absent'], (m, s) -> # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d state = s.trim() match = m.getFullMatch() ) ) if match? assert device? assert state in ['present', 'absent'] assert typeof match is "string" state = (state is 'present') return { token: match nextInput: input.substring(match.length) actionHandler: new PresenceActionHandler(device, state) } else return null class PresenceActionHandler extends ActionHandler constructor: (@device, @state) -> setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate, state) => return ( if simulate if state then Promise.resolve __("would set presence of %s to present", @device.name) else Promise.resolve __("would set presence of %s to absent", @device.name) else if state then @device.changePresenceTo(state).then( => __("set presence of %s to present", @device.name) ) else @device.changePresenceTo(state).then( => __("set presence %s to absent", @device.name) ) ) # ### executeAction() executeAction: (simulate) => @_doExecuteAction(simulate, @state) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => @_doExecuteAction(simulate, (not @state)) ### The open/close ActionProvider ------------- Provides open/close action, for the DummyContactSensor. ### class ContactActionProvider extends ActionProvider constructor: (@framework) -> parseAction: (input, context) -> retVar = null contactDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("changeContactTo") ).value() device = null state = null match = null m = M(input, context).match(['open ', 'close '], (m, a) => m.matchDevice(contactDevices, (m, d) -> if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d state = a.trim() match = m.getFullMatch() ) ) if match? assert device? assert state in ['open', 'close'] assert typeof match is "string" state = (state is 'close') return { token: match nextInput: input.substring(match.length) actionHandler: new ContactActionHandler(device, state) } else return null class ContactActionHandler extends ActionHandler constructor: (@device, @state) -> setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate, state) => return ( if simulate if state then Promise.resolve __("would set contact %s to closed", @device.name) else Promise.resolve __("would set contact %s to opened", @device.name) else if state then @device.changeContactTo(state).then( => __("set contact %s to closed", @device.name) ) else @device.changeContactTo(state).then( => __("set contact %s to opened", @device.name) ) ) # ### executeAction() executeAction: (simulate) => @_doExecuteAction(simulate, @state) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => @_doExecuteAction(simulate, (not @state)) ### The Switch Action Provider ------------- Provides the ability to switch devices on or off. Currently it handles the following actions: * switch [the] _device_ on|off * turn [the] _device_ on|off * switch on|off [the] _device_ * turn on|off [the] _device_ where _device_ is the name or id of a device and "the" is optional. ### class SwitchActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: retVar = null switchDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("turnOn") and device.hasAction("turnOff") ).value() device = null state = null match = null # Try to match the input string with: turn|switch -> m = M(input, context).match(['turn ', 'switch ']) # device name -> on|off m.matchDevice(switchDevices, (m, d) -> m.match([' on', ' off'], (m, s) -> # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d state = s.trim() match = m.getFullMatch() ) ) # on|off -> deviceName m.match(['on ', 'off '], (m, s) -> m.matchDevice(switchDevices, (m, d) -> # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d state = s.trim() match = m.getFullMatch() ) ) if match? assert device? assert state in ['on', 'off'] assert typeof match is "string" state = (state is 'on') return { token: match nextInput: input.substring(match.length) actionHandler: new SwitchActionHandler(device, state) } else return null class SwitchActionHandler extends ActionHandler constructor: (@device, @state) -> setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate, state) => return ( if simulate if state then Promise.resolve __("would turn %s on", @device.name) else Promise.resolve __("would turn %s off", @device.name) else if state then @device.turnOn().then( => __("turned %s on", @device.name) ) else @device.turnOff().then( => __("turned %s off", @device.name) ) ) # ### executeAction() executeAction: (simulate) => @_doExecuteAction(simulate, @state) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => @_doExecuteAction(simulate, (not @state)) ### The Toggle Action Provider ------------- Provides the ability to toggle switch devices on or off. Currently it handles the following actions: * toggle the state of _device_ * toggle the state of [the] _device_ * toggle _device_ state * toggle [the] _device_ state where _device_ is the name or id of a device and "the" is optional. ### class ToggleActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: retVar = null switchDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("toggle") ).value() if switchDevices.length is 0 then return device = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match('toggle ') .or([ ( (m) => return m.match('the state of ', optional: yes) .matchDevice(switchDevices, onDeviceMatch) ), ( (m) => return m.matchDevice(switchDevices, (m, d) -> return m.match(' state', optional: yes, (m)-> onDeviceMatch(m, d) ) ) ) ]) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new ToggleActionHandler(device) } else return null class ToggleActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() # ### executeAction() executeAction: (simulate) => return ( if simulate Promise.resolve __("would toggle state of %s", @device.name) else @device.toggle().then( => __("toggled state of %s", @device.name) ) ) ### The Button Action Provider ------------- Provides the ability to press the button of a buttonsdevices. Currently it handles the following actions: * press [the] _device_ where _device_ is the name or id of a the button not the buttons device and "the" is optional. ### class ButtonActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: matchCount = 0 matchingDevice = null matchingButtonId = null end = () => matchCount++ onButtonMatch = (m, {device, buttonId}) => matchingDevice = device matchingButtonId = buttonId buttonsWithId = [] for id, d of @framework.deviceManager.devices continue unless d instanceof env.devices.ButtonsDevice for b in d.config.buttons buttonsWithId.push [{device: d, buttonId: b.id}, b.id] buttonsWithId.push [{device: d, buttonId: b.id}, b.text] if b.id isnt b.text m = M(input, context) .match('press ') .match('the ', optional: true) .match('button ', optional: true) .match( buttonsWithId, wildcard: "{button}", onButtonMatch ) match = m.getFullMatch() if match? assert matchingDevice? assert matchingButtonId? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new ButtonActionHandler(matchingDevice, matchingButtonId) } else return null class ButtonActionHandler extends ActionHandler constructor: (@device, @buttonId) -> assert @device? and @device instanceof env.devices.ButtonsDevice assert @buttonId? and typeof @buttonId is "string" setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate) => return ( if simulate Promise.resolve __("would press button %s of device %s", @buttonId, @device.id) else @device.buttonPressed(@buttonId) .then( =>__("press button %s of device %s", @buttonId, @device.id) ) ) # ### executeAction() executeAction: (simulate) => @_doExecuteAction(simulate) # ### hasRestoreAction() hasRestoreAction: -> no ### The Shutter Action Provider ------------- Provides the ability to raise or lower a shutter * lower [the] _device_ [down] * raise [the] _device_ [up] * move [the] _device_ up|down where _device_ is the name or id of a device and "the" is optional. ### class ShutterActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => shutterDevices = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("moveUp") and device.hasAction("moveDown") ).value() device = null position = null match = null # Try to match the input string with: raise|up -> m = M(input, context).match(['raise ', 'lower ', 'move '], (m, a) => # device name -> up|down m.matchDevice(shutterDevices, (m, d) -> [p, nt] = ( switch a.trim() when 'raise' then ['up', ' up'] when 'lower' then ['down', ' down'] else [null, [" up", " down"] ] ) last = m.match(nt, {optional: a.trim() isnt 'move'}, (m, po) -> p = po.trim() ) if last.hadMatch() # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d position = p match = last.getFullMatch() ) ) if match? assert device? assert position in ['down', 'up'] assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new ShutterActionHandler(device, position) } else return null class ShutterActionHandler extends ActionHandler constructor: (@device, @position) -> setup: -> @dependOnDevice(@device) super() # ### executeAction() executeAction: (simulate) => return ( if simulate if @position is 'up' then Promise.resolve __("would raise %s", @device.name) else Promise.resolve __("would lower %s", @device.name) else if @position is 'up' then @device.moveUp().then( => __("raised %s", @device.name) ) else @device.moveDown().then( => __("lowered %s", @device.name) ) ) # ### hasRestoreAction() hasRestoreAction: -> @device.hasAction('stop') # ### executeRestoreAction() executeRestoreAction: (simulate) => if simulate then Promise.resolve __("would stop %s", @device.name) else @device.stop().then( => __("stopped %s", @device.name) ) ### The Shutter Stop Action Provider ------------- Provides the ability to stop a shutter * stop [the] _device_ where _device_ is the name or id of a device and "the" is optional. ### class StopShutterActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => shutterDevices = _(@framework.deviceManager.devices).values().filter( # only match Shutter devices and not media players (device) => device.hasAction("stop") and device.hasAction("moveUp") ).value() device = null match = null # Try to match the input string with: stop -> m = M(input, context).match("stop ", (m, a) => # device name -> up|down m.matchDevice(shutterDevices, (m, d) -> # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d match = m.getFullMatch() ) ) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new StopShutterActionHandler(device) } else return null class StopShutterActionHandler extends ActionHandler constructor: (@device) -> setup: -> @dependOnDevice(@device) super() # ### executeAction() executeAction: (simulate) => return ( if simulate Promise.resolve __("would stop %s", @device.name) else @device.stop().then( => __("stopped %s", @device.name) ) ) # ### hasRestoreAction() hasRestoreAction: -> false ### The Dimmer Action Provider ------------- Provides the ability to change the dim level of dimmer devices. Currently it handles the following actions: * dim [the] _device_ to _value_% where _device_ is the name or id of a device and "the" is optional. ### class DimmerActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: retVar = null dimmers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("changeDimlevelTo") ).value() if dimmers.length is 0 then return device = null valueTokens = null match = null # Try to match the input string with: M(input, context) .match('dim ') .matchDevice(dimmers, (next, d) => next.match(' to ') .matchNumericExpression( (next, ts) => m = next.match('%', optional: yes) if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d valueTokens = ts match = m.getFullMatch() ) ) if match? if valueTokens.length is 1 and not isNaN(valueTokens[0]) value = valueTokens[0] assert(not isNaN(value)) value = parseFloat(value) if value < 0.0 context?.addError("Can't dim to a negative dimlevel.") return if value > 100.0 context?.addError("Can't dim to greater than 100%.") return return { token: match nextInput: input.substring(match.length) actionHandler: new DimmerActionHandler(@framework, device, valueTokens) } else return null class DimmerActionHandler extends ActionHandler constructor: (@framework, @device, @valueTokens) -> assert @device? assert @valueTokens? setup: -> @dependOnDevice(@device) super() _clampVal: (value) -> assert(not isNaN(value)) return (switch when value > 100 then 100 when value < 0 then 0 else value ) ### Handles the above actions. ### _doExecuteAction: (simulate, value) => return ( if simulate __("would dim %s to %s%%", @device.name, value) else @device.changeDimlevelTo(value).then( => __("dimmed %s to %s%%", @device.name, value) ) ) # ### executeAction() executeAction: (simulate) => @device.getDimlevel().then( (lastValue) => @lastValue = lastValue or 0 return @framework.variableManager.evaluateNumericExpression(@valueTokens).then( (value) => value = @_clampVal value return @_doExecuteAction(simulate, value) ) ) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => Promise.resolve(@_doExecuteAction(simulate, @lastValue)) class HeatingThermostatModeActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: retVar = null thermostats = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("changeModeTo") ).value() if thermostats.length is 0 then return device = null valueTokens = null match = null # Try to match the input string with: M(input, context) .match('set mode of ') .matchDevice(thermostats, (next, d) => next.match(' to ') .matchStringWithVars( (next, ts) => m = next.match(' mode', optional: yes) if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d valueTokens = ts match = m.getFullMatch() ) ) if match? if valueTokens.length is 1 and not isNaN(valueTokens[0]) value = valueTokens[0] assert(not isNaN(value)) modes = ["eco", "boost", "auto", "manu", "comfy"] # TODO: Implement eco & comfy in changeModeTo method! if modes.indexOf(value) < -1 context?.addError("Allowed modes: eco,boost,auto,manu,comfy") return return { token: match nextInput: input.substring(match.length) actionHandler: new HeatingThermostatModeActionHandler(@framework, device, valueTokens) } else return null class HeatingThermostatModeActionHandler extends ActionHandler constructor: (@framework, @device, @valueTokens) -> assert @device? assert @valueTokens? setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate, value) => return ( if simulate __("would set mode %s to %s", @device.name, value) else @device.changeModeTo(value).then( => __("set mode %s to %s", @device.name, value) ) ) # ### executeAction() executeAction: (simulate) => @framework.variableManager.evaluateStringExpression(@valueTokens).then( (value) => @lastValue = value return @_doExecuteAction(simulate, value) ) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => Promise.resolve(@_doExecuteAction(simulate, @lastValue)) class HeatingThermostatSetpointActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => # The result the function will return: retVar = null thermostats = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("changeTemperatureTo") ).value() if thermostats.length is 0 then return device = null valueTokens = null match = null # Try to match the input string with: M(input, context) .match('set temp of ') .matchDevice(thermostats, (next, d) => next.match(' to ') .matchNumericExpression( (next, ts) => m = next.match('°C', optional: yes) if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d valueTokens = ts match = m.getFullMatch() ) ) if match? if valueTokens.length is 1 and not isNaN(valueTokens[0]) value = valueTokens[0] assert(not isNaN(value)) value = parseFloat(value) if value < 0.0 context?.addError("Can't set temp to a negative value.") return if value > 32.0 context?.addError("Can't set temp higher than 32°C.") return return { token: match nextInput: input.substring(match.length) actionHandler: new HeatingThermostatSetpointActionHandler(@framework, device, valueTokens) } else return null class HeatingThermostatSetpointActionHandler extends ActionHandler constructor: (@framework, @device, @valueTokens) -> assert @device? assert @valueTokens? setup: -> @dependOnDevice(@device) super() ### Handles the above actions. ### _doExecuteAction: (simulate, value) => return ( if simulate __("would set temp of %s to %s°C", @device.name, value) else @device.changeTemperatureTo(value).then( => __("set temp of %s to %s°C", @device.name, value) ) ) # ### executeAction() executeAction: (simulate) => @framework.variableManager.evaluateNumericExpression(@valueTokens).then( (value) => # value = @_clampVal value @lastValue = value return @_doExecuteAction(simulate, value) ) # ### hasRestoreAction() hasRestoreAction: -> yes # ### executeRestoreAction() executeRestoreAction: (simulate) => Promise.resolve(@_doExecuteAction(simulate, @lastValue)) ### The Timer Action Provider ------------- Start, stop or reset Timer * start|stop|reset the _device_ [timer] where _device_ is the name or id of a timer device and "the" is optional. ### class TimerActionProvider extends ActionProvider constructor: (@framework) -> # ### parseAction() ### Parses the above actions. ### parseAction: (input, context) => timerDevices = _(@framework.deviceManager.devices).values().filter( (device) => ( device.hasAction("startTimer") and device.hasAction("stopTimer") and device.hasAction("resetTimer") ) ).value() device = null action = null match = null # Try to match the input string with: start|stop|reset -> m = M(input, context).match(['start ', 'stop ', 'reset '], (m, a) => # device name -> up|down m.matchDevice(timerDevices, (m, d) -> last = m.match(' timer', {optional: yes}) if last.hadMatch() # Already had a match with another device? if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d action = a.trim() match = last.getFullMatch() ) ) if match? assert device? assert action in ['start', 'stop', 'reset'] assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new TimerActionHandler(device, action) } return null class TimerActionHandler extends ActionHandler constructor: (@device, @action) -> setup: -> @dependOnDevice(@device) super() # ### executeAction() executeAction: (simulate) => return ( if simulate Promise.resolve __("would #{@action} %s", @device.name) else @device["#{@action}Timer"]().then( => __("#{@action}ed %s", @device.name) ) ) # ### hasRestoreAction() hasRestoreAction: -> false # Pause play volume actions class AVPlayerPauseActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `play device` ### parseAction: (input, context) => retVar = null avPlayers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("pause") ).value() if avPlayers.length is 0 then return device = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match('pause ') .matchDevice(avPlayers, onDeviceMatch) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerPauseActionHandler(device) } else return null class AVPlayerPauseActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate) => return ( if simulate Promise.resolve __("would pause %s", @device.name) else @device.pause().then( => __("paused %s", @device.name) ) ) # stop play volume actions class AVPlayerStopActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `execute "some string"` ### parseAction: (input, context) => retVar = null avPlayers = _(@framework.deviceManager.devices).values().filter( # only match media players and not shutters (device) => device.hasAction("stop") and device.hasAction("play") ).value() if avPlayers.length is 0 then return device = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match('stop ') .matchDevice(avPlayers, onDeviceMatch) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerStopActionHandler(device) } else return null class AVPlayerStopActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate) => return ( if simulate Promise.resolve __("would stop %s", @device.name) else @device.stop().then( => __("stop %s", @device.name) ) ) class AVPlayerPlayActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `execute "some string"` ### parseAction: (input, context) => retVar = null avPlayers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("play") ).value() if avPlayers.length is 0 then return device = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match('play ') .matchDevice(avPlayers, onDeviceMatch) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerPlayActionHandler(device) } else return null class AVPlayerPlayActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate) => return ( if simulate Promise.resolve __("would play %s", @device.name) else @device.play().then( => __("playing %s", @device.name) ) ) class AVPlayerVolumeActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `execute "some string"` ### parseAction: (input, context) => retVar = null volume = null avPlayers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("setVolume") ).value() if avPlayers.length is 0 then return device = null valueTokens = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) M(input, context) .match('change volume of ') .matchDevice(avPlayers, (next,d) => next.match(' to ', (next) => next.matchNumericExpression( (next, ts) => m = next.match('%', optional: yes) if device? and device.id isnt d.id context?.addError(""""#{input.trim()}" is ambiguous.""") return device = d valueTokens = ts match = m.getFullMatch() ) ) ) if match? value = valueTokens[0] assert device? assert typeof match is "string" value = parseFloat(value) if value < 0.0 context?.addError("Can't change volume to a negative value.") return if value > 100.0 context?.addError("Can't change volume to greater than 100%.") return return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerVolumeActionHandler(@framework,device,valueTokens) } else return null class AVPlayerVolumeActionHandler extends ActionHandler constructor: (@framework, @device, @valueTokens) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate, value) => return ( if isNaN(@valueTokens[0]) val = @framework.variableManager.getVariableValue(@valueTokens[0].substring(1)) else val = @valueTokens[0] if simulate Promise.resolve __("would set volume of %s to %s", @device.name, val) else @device.setVolume(val).then( => __("set volume of %s to %s", @device.name, val) ) ) class AVPlayerNextActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `execute "some string"` ### parseAction: (input, context) => retVar = null volume = null avPlayers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("next") ).value() if avPlayers.length is 0 then return device = null valueTokens = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match(['play next', 'next ']) .match(" song ", optional: yes) .match("on ", optional: yes) .matchDevice(avPlayers, onDeviceMatch) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerNextActionHandler(device) } else return null class AVPlayerNextActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate) => return ( if simulate Promise.resolve __("would play next track of %s", @device.name) else @device.next().then( => __("play next track of %s", @device.name) ) ) class AVPlayerPrevActionProvider extends ActionProvider constructor: (@framework) -> # ### executeAction() ### This function handles action in the form of `execute "some string"` ### parseAction: (input, context) => retVar = null volume = null avPlayers = _(@framework.deviceManager.devices).values().filter( (device) => device.hasAction("previous") ).value() if avPlayers.length is 0 then return device = null valueTokens = null match = null onDeviceMatch = ( (m, d) -> device = d; match = m.getFullMatch() ) m = M(input, context) .match(['play previous', 'previous ']) .match(" song ", optional: yes) .match("on ", optional: yes) .matchDevice(avPlayers, onDeviceMatch) if match? assert device? assert typeof match is "string" return { token: match nextInput: input.substring(match.length) actionHandler: new AVPlayerNextActionHandler(device) } else return null class AVPlayerPrevActionHandler extends ActionHandler constructor: (@device) -> #nop setup: -> @dependOnDevice(@device) super() executeAction: (simulate) => return ( if simulate Promise.resolve __("would play previous track of %s", @device.name) else @device.previous().then( => __("play previous track of %s", @device.name) ) ) # Export the classes so that they can be accessed by the framework return exports = { ActionHandler ActionProvider SetVariableActionProvider SetPresenceActionProvider ContactActionProvider SwitchActionProvider DimmerActionProvider LogActionProvider ShutterActionProvider StopShutterActionProvider ToggleActionProvider ButtonActionProvider HeatingThermostatModeActionProvider HeatingThermostatSetpointActionProvider TimerActionProvider AVPlayerPauseActionProvider AVPlayerStopActionProvider AVPlayerPlayActionProvider AVPlayerVolumeActionProvider AVPlayerNextActionProvider AVPlayerPrevActionProvider }