pimatic
Version:
A home automation server and framework for the Raspberry PI running on node.js
895 lines (761 loc) • 27.4 kB
text/coffeescript
###
Predicate Provider
=================
A Predicate Provider provides a predicate for the Rule System. For predicate and rule explanations
take a look at the [rules file](rules.html). A predicate is a string that describes a state. A
predicate is either true or false at a given time. There are special predicates,
called event-predicates, that represent events. These predicate are just true in the moment a
special event happen.
###
__ = require("i18n-pimatic").__
Promise = require 'bluebird'
S = require 'string'
assert = require 'cassert'
_ = require 'lodash'
M = require './matcher'
types = require('decl-api').types
module.exports = (env) ->
###
The Predicate Provider
----------------
This is the base class for all predicate provider.
###
class PredicateProvider
parsePredicate: (input, context) -> throw new Error("You must implement parsePredicate")
class PredicateHandler extends require('events').EventEmitter
getType: -> throw new Error("You must implement getType")
getValue: -> throw new Error("You must implement getValue")
setup: ->
# You must overwrite this method and set up your listener here.
# You should call super() after that.
if then throw new Error("Setup already called!")
= yes
destroy: ->
# You must overwrite this method and remove your listener here.
# You should call super() after that.
unless then throw new Error("Destroy called, but setup was not called!")
delete
"destroy"
dependOnDevice: (device) ->
recreateEmitter = (=> "recreate")
device.on "changed", recreateEmitter
device.on "destroyed", recreateEmitter
'destroy', =>
device.removeListener "changed", recreateEmitter
device.removeListener "destroyed", recreateEmitter
dependOnVariable: (variableManager, varName) ->
recreateEmitter = ( (variable) =>
if variable.name isnt varName
return
"recreate"
)
variableManager.on "variableRemoved", recreateEmitter
'destroy', =>
variableManager.removeListener "variableRemoved", recreateEmitter
###
The Switch Predicate Provider
----------------
Provides predicates for the state of switch devices like:
* _device_ is on|off
* _device_ is switched on|off
* _device_ is turned on|off
####
class SwitchPredicateProvider extends PredicateProvider
presets: [
{
name: "switch turned on/off"
input: "{device} is turned on"
}
]
constructor: () ->
# ### parsePredicate()
parsePredicate: (input, context) ->
switchDevices = _(.deviceManager.devices).values()
.filter((device) => device.hasAttribute( 'state')).value()
device = null
state = null
match = null
stateAcFilter = (v) => v.trim() isnt 'is switched'
M(input, context)
.matchDevice(switchDevices, (next, d) =>
next.match([' is', ' is turned', ' is switched'], acFilter: stateAcFilter, type: 'static')
.match([' on', ' off'], param: 'state', type: 'select', (next, s) =>
# Already had a match with another device?
if device? and device.id isnt d.id
context?.addError(""""#{input.trim()}" is ambiguous.""")
return
assert d?
assert s in [' on', ' off']
device = d
state = s.trim() is 'on'
match = next.getFullMatch()
)
)
# If we have a match
if match?
assert device?
assert state?
assert typeof match is "string"
# and state as boolean.
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new SwitchPredicateHandler(device, state)
}
else
return null
class SwitchPredicateHandler extends PredicateHandler
constructor: (, ) ->
setup: ->
= (s) => 'change', (s is )
.on 'state',
super()
getValue: -> .getUpdatedAttributeValue('state').then( (s) => (s is ) )
destroy: ->
.removeListener "state",
super()
getType: -> 'state'
###
The Presence Predicate Provider
----------------
Handles predicates of presence devices like
* _device_ is present
* _device_ is not present
* _device_ is absent
####
class PresencePredicateProvider extends PredicateProvider
presets: [
{
name: "device is present/absent"
input: "{device} is present"
}
]
constructor: () ->
parsePredicate: (input, context) ->
presenceDevices = _(.deviceManager.devices).values()
.filter((device) => device.hasAttribute( 'presence')).value()
device = null
negated = null
match = null
stateAcFilter = (v) => v.trim() isnt 'not present'
M(input, context)
.matchDevice(presenceDevices, (next, d) =>
next.match([' is', ' reports', ' signals'], type: "static")
.match(
[' present', ' absent', ' not present'],
acFilter: stateAcFilter, type: "select", param: "state",
(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
negated = (s.trim() isnt "present")
match = m.getFullMatch()
)
)
if match?
assert device?
assert negated?
assert typeof match is "string"
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new PresencePredicateHandler(device, negated)
}
else
return null
class PresencePredicateHandler extends PredicateHandler
constructor: (, ) ->
setup: ->
= (p) =>
'change', (if then not p else p)
.on 'presence',
super()
getValue: ->
return .getUpdatedAttributeValue('presence').then(
(p) => (if then not p else p)
)
destroy: ->
.removeListener "presence",
super()
getType: -> 'state'
###
The Contact Predicate Provider
----------------
Handles predicates of contact devices like
* _device_ is opened
* _device_ is closed
####
class ContactPredicateProvider extends PredicateProvider
presets: [
{
name: "device is opened/closed"
input: "{device} is opened"
}
]
constructor: () ->
parsePredicate: (input, context) ->
contactDevices = _(.deviceManager.devices).values()
.filter((device) => device.hasAttribute( 'contact')).value()
device = null
negated = null
match = null
contactAcFilter = (v) => v.trim() in ['opened', 'closed']
M(input, context)
.matchDevice(contactDevices, (next, d) =>
next.match(' is', type: "static")
.match(
[' open', ' close', ' opened', ' closed'],
acFilter: contactAcFilter, type: "select",
(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
negated = (s.trim() in ["opened", 'open'])
match = m.getFullMatch()
)
)
if match?
assert device?
assert negated?
assert typeof match is "string"
return {
token: match
nextInput: input.substring(match.length),
predicateHandler: new ContactPredicateHandler(device, negated)
}
else
return null
class ContactPredicateHandler extends PredicateHandler
constructor: (, ) ->
setup: ->
= (p) =>
'change', (if then not p else p)
.on 'contact',
super()
getValue: -> .getUpdatedAttributeValue('contact').then(
(p) => (if then not p else p)
)
destroy: ->
.removeListener "contact",
super()
getType: -> 'state'
###
The Device-Attribute Predicate Provider
----------------
Handles predicates for comparing device attributes like sensor values or other states:
* _attribute_ of _device_ is equal to _value_
* _attribute_ of _device_ equals _value_
* _attribute_ of _device_ is not _value_
* _attribute_ of _device_ is less than _value_
* _attribute_ of _device_ is lower than _value_
* _attribute_ of _device_ is greater than _value_
* _attribute_ of _device_ is higher than _value_
####
class DeviceAttributePredicateProvider extends PredicateProvider
presets: [
{
name: "attribute of a device"
input: "{attribute} of {device} is equal to {value}"
}
]
constructor: () ->
# ### parsePredicate()
parsePredicate: (input, context) ->
allAttributes = _(.deviceManager.getDevices())
.map((device) => _.keys(device.attributes))
.flatten().uniq().value()
result = null
matches = []
M(input, context)
.match(
allAttributes,
param: "attribute", wildcard: "{attribute}"
(m, attr) =>
info = {
device: null
attributeName: null
comparator: null
referenceValue: null
}
info.attributeName = attr
devices = _(.deviceManager.devices).values()
.filter((device) => device.hasAttribute(attr)).value()
m.match(' of ').matchDevice(devices, (next, device) =>
info.device = device
unless device.hasAttribute(attr) then return
attribute = device.attributes[attr]
setComparator = (m, c) => info.comparator = c
setRefValue = (m, v) => info.referenceValue = v
end = => matchCount++
if attribute.type is types.boolean
m = next.matchComparator('boolean', setComparator)
.match(attribute.labels, wildcard: '{value}', (m, v) =>
if v is attribute.labels[0] then setRefValue(m, true)
else if v is attribute.labels[1] then setRefValue(m, false)
else assert(false)
)
else if attribute.type is types.number
m = next.matchComparator('number', setComparator)
.matchNumber(wildcard: '{value}', (m,v) => setRefValue(m, parseFloat(v)) )
if attribute.unit? and attribute.unit.length > 0
possibleUnits = _.uniq([
" #{attribute.unit}",
"#{attribute.unit}",
"#{attribute.unit.toLowerCase()}",
" #{attribute.unit.toLowerCase()}",
"#{attribute.unit.replace('°', '')}",
" #{attribute.unit.replace('°', '')}",
"#{attribute.unit.toLowerCase().replace('°', '')}",
" #{attribute.unit.toLowerCase().replace('°', '')}",
])
autocompleteFilter = (v) => v is " #{attribute.unit}"
m = m.match(possibleUnits, {optional: yes, acFilter: autocompleteFilter})
else if attribute.type is types.string
m = next.matchComparator('string', setComparator)
.or([
( (m) => m.matchString(wildcard: '{value}', setRefValue) ),
( (m) =>
if attribute.enum?
m.match(attribute.enum, wildcard: '{value}', setRefValue)
else M(null)
)
])
if m.hadMatch()
matches.push m.getFullMatch()
if result?
if result.device.id isnt info.device.id or
result.attributeName isnt info.attributeName
context?.addError(""""#{input.trim()}" is ambiguous.""")
result = info
)
)
if result?
assert result.device?
assert result.attributeName?
assert result.comparator?
assert result.referenceValue?
# take the longest match
match = _(matches).sortBy( (s) => s.length ).last()
assert typeof match is "string"
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new DeviceAttributePredicateHandler(
result.device, result.attributeName, result.comparator, result.referenceValue
)
}
return null
class DeviceAttributePredicateHandler extends PredicateHandler
constructor: (, , , ) ->
setup: ->
lastState = null
= (value) =>
state =
if state isnt lastState
lastState = state
'change', state
.on ,
super()
getValue: ->
.getUpdatedAttributeValue().then( (value) =>
)
destroy: ->
.removeListener ,
super()
getType: -> 'state'
# ### _compareValues()
###
Does the comparison.
###
_compareValues: (comparator, value, referenceValue) ->
if typeof referenceValue is "number"
value = parseFloat(value)
result = switch comparator
when '==' then value is referenceValue
when '!=' then value isnt referenceValue
when '<' then value < referenceValue
when '>' then value > referenceValue
when '<=' then value <= referenceValue
when '>=' then value >= referenceValue
else throw new Error "Unknown comparator: #{comparator}"
return result
###
The Device-Attribute Watchdog Provider
----------------
Handles predicates that will become true if a attribute of a device was not updated for a
certain time.
* _attribute_ of _device_ was not updated for _time_
####
class DeviceAttributeWatchdogProvider extends PredicateProvider
presets: [
{
name: "attribute of a device not updated"
input: "{attribute} of {device} was not updated for {duration} minutes"
}
]
constructor: () ->
# ### parsePredicate()
parsePredicate: (input, context) ->
allAttributes = _(.deviceManager.getDevices())
.map((device) => _.keys(device.attributes))
.flatten().uniq().value()
result = null
match = null
M(input, context)
.match(allAttributes, wildcard: "{attribute}", type: "select", (m, attr) =>
info = {
device: null
attributeName: null
timeMs: null
}
info.attributeName = attr
devices = _(.deviceManager.devices).values()
.filter( (device) => device.hasAttribute(attr) ).value()
m.match(' of ').matchDevice(devices, (m, device) =>
info.device = device
unless device.hasAttribute(attr) then return
attribute = device.attributes[attr]
m.match(' was not updated for ', type: "static")
.matchTimeDuration(wildcard: "{duration}", type: "text", (m, {time, unit, timeMs}) =>
info.timeMs = timeMs
result = info
match = m.getFullMatch()
)
)
)
if result?
assert result.device?
assert result.attributeName?
assert result.timeMs?
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new DeviceAttributeWatchdogPredicateHandler(
result.device, result.attributeName, result.timeMs
)
}
return null
class DeviceAttributeWatchdogPredicateHandler extends PredicateHandler
constructor: (, , ) ->
setup: ->
= false
= ( =>
if is true
= false
'change', false
)
.on ,
super()
getValue: -> Promise.resolve()
destroy: ->
.removeListener ,
clearTimeout()
super()
getType: -> 'state'
_rescheduleTimeout: ->
clearTimeout()
= setTimeout( ( =>
= true
'change', true
), )
###
The Variable Predicate Provider
----------------
Handles comparison of variables
####
class VariablePredicateProvider extends PredicateProvider
presets: [
{
name: "Variable comparison"
input: "{expr} = {expr}"
}
]
constructor: () ->
parsePredicate: (input, context) ->
result = null
M(input, context)
.matchAnyExpression( (next, leftTokens) =>
next.matchComparator('number', (next, comparator) =>
next.matchAnyExpression( (next, rightTokens) =>
result = {
leftTokens
rightTokens
comparator
match: next.getFullMatch()
}
)
)
)
if result?
assert Array.isArray result.leftTokens
assert Array.isArray result.rightTokens
assert result.comparator in ['==', '!=', '<', '>', '<=', '>=']
assert typeof result.match is "string"
variables = .variableManager.extractVariables(
result.leftTokens.concat result.rightTokens
)
for v in variables?
unless .variableManager.isVariableDefined(v)
context.addError("Variable $#{v} is not defined.")
return null
return {
token: result.match
nextInput: input.substring(result.match.length)
predicateHandler: new VariablePredicateHandler(
, result.leftTokens, result.rightTokens, result.comparator
)
}
else
return null
class VariablePredicateHandler extends PredicateHandler
constructor: (, , , ) ->
setup: ->
= null
= .variableManager.extractVariables(
.concat
)
for variable in
= (variable, value) =>
unless variable.name in then return
evalPromise =
evalPromise.then( (state) =>
if state isnt
= state
'change', state
).catch( (error) =>
env.logger.error "Error in VariablePredicateHandler:", error.message
env.logger.debug error
)
.variableManager.on("variableValueChanged", )
super()
getValue: ->
return
destroy: ->
.variableManager.removeListener("variableValueChanged", )
super()
getType: -> 'state'
_evaluate: ->
leftPromise = .variableManager.evaluateExpression()
rightPromise = .variableManager.evaluateExpression()
return Promise.all([leftPromise, rightPromise]).then( ([leftValue, rightValue]) =>
return state =
)
# ### _compareValues()
###
Does the comparison.
###
_compareValues: (left, right) ->
if in ["<", ">", "<=", ">="]
if typeof left is "string"
if isNaN(left)
throw new Error("Can not compare strings with #{@comparator}!")
left = parseFloat(left)
if typeof right is "string"
if isNaN(right)
throw new Error("Can not compare strings with #{@comparator}!")
right = parseFloat(right)
return switch
when '==' then left is right
when '!=' then left isnt right
when '<' then left < right
when '>' then left > right
when '<=' then left <= right
when '>=' then left >= right
else throw new Error "Unknown comparator: #{@comparator}"
class VariableUpdatedPredicateProvider extends PredicateProvider
presets: [
{
name: "Variable changes"
input: "{variable} changes"
}
{
name: "Variable increased/decreased"
input: "{variable} increased"
}
]
constructor: () ->
parsePredicate: (input, context) ->
variableName = null
mode = null
setVariableName = (next, name) => variableName = name.substring(1)
setMode = (next, match) => mode = match.trim()
m = M(input, context)
.matchVariable(setVariableName)
.match([
" changes", " gets updated",
" increased", " decreased",
" is increasing", " is decreasing"
], setMode)
if m.hadMatch()
match = m.getFullMatch()
assert typeof variableName is "string"
assert mode?
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new VariableUpdatedPredicateHandler(
, variableName, mode
)
}
else
return null
class VariableUpdatedPredicateHandler extends PredicateHandler
constructor: (, , ) ->
setup: ->
= null
= false
= (variable, value) =>
unless variable.name is then return
switch
when 'changes'
if isnt value
'change', "event"
when 'gets updated'
'change', "event"
when 'increased'
if value >
'change', "event"
when 'decreased'
if value <
'change', "event"
when 'is increasing'
if value >
if not
= true
'change', true
else
if
= false
'change', false
when 'is decreasing'
if value <
if not
= true
'change', true
else
if
= false
'change', false
= value
.variableManager.on("variableValueChanged", )
super()
getValue: -> Promise.resolve()
destroy: ->
.variableManager.removeListener("variableValueChanged", )
super()
getType: ->
switch
when 'is increasing', 'is decreasing' then return 'state'
else return 'event'
class ButtonPredicateProvider extends PredicateProvider
presets: [
{
name: "Button pressed"
input: "{button} is pressed"
}
]
constructor: () ->
parsePredicate: (input, context) ->
matchCount = 0
matchingDevice = null
matchingButtonId = null
end = () => matchCount++
onButtonMatch = (m, {device, buttonId}) =>
matchingDevice = device
matchingButtonId = buttonId
buttonsWithId = []
for id, d of .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('the ', optional: true)
.match(
buttonsWithId,
wildcard: "{button}"
onButtonMatch
)
.match(' button', optional: true)
.match(' is', optional: true)
.match(' pressed')
if m.hadMatch()
match = m.getFullMatch()
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new ButtonPredicateHandler(this, matchingDevice, matchingButtonId)
}
return null
class ButtonPredicateHandler extends PredicateHandler
constructor: (, , ) ->
assert ? and instanceof env.devices.ButtonsDevice
assert ? and typeof is "string"
setup: ->
= ( (id) =>
if id is
'change', 'event'
)
.on 'button',
super()
getValue: -> Promise.resolve(false)
destroy: ->
.removeListener 'button',
super()
getType: -> 'event'
class StartupPredicateProvider extends PredicateProvider
presets: [
{
name: "pimatic is starting"
input: "pimatic is starting"
}
]
constructor: () ->
parsePredicate: (input, context) ->
m = M(input, context).match(["pimatic is starting"])
if m.hadMatch()
match = m.getFullMatch()
return {
token: match
nextInput: input.substring(match.length)
predicateHandler: new StartupPredicateHandler()
}
else
return null
class StartupPredicateHandler extends PredicateHandler
constructor: () ->
setup: ->
.once "after init", =>
'change', "event"
super()
getValue: -> Promise.resolve(false)
getType: -> 'event'
return exports = {
PredicateProvider
PredicateHandler
PresencePredicateProvider
SwitchPredicateProvider
DeviceAttributePredicateProvider
VariablePredicateProvider
VariableUpdatedPredicateProvider
ContactPredicateProvider
ButtonPredicateProvider
DeviceAttributeWatchdogProvider
StartupPredicateProvider
}