pimatic-fritz
Version:
Connect pimatic to FritzBox, FritzDECT and CometDECT devices
562 lines (476 loc) • 17.7 kB
text/coffeescript
# #Fritz plugin
module.exports = (env) ->
# libraries
Promise = env.require 'bluebird'
assert = env.require 'cassert'
t = env.require('decl-api').types
_ = env.require 'lodash'
# Require https://github.com/andig/fritzapi
fritz = require 'fritzapi'
# ###FritzPlugin class
class FritzPlugin extends env.plugins.Plugin
# Fritz session id
sid: null
# ####init()
# The `init` function is called by the framework to ask your plugin to initialise.
#
# #####params:
# * `app` is the [express] instance the framework is using.
# * `framework` the framework itself
# * `config` the properties the user specified as config for your plugin in the `plugins`
# section of the config.json file
#
init: (app, @framework, @config) =>
# register devices
deviceConfigDef = require("./device-config-schema")
# switch list
@fritzCall("getSwitchList")
.then (ains) =>
env.logger.info "Switch AINs: " + ains
# thermostat list
@fritzCall("getThermostatList")
.then (ains) =>
env.logger.info "Thermostat AINs: " + ains
@fritzCall("getDeviceListFiltered", { functionbitmask: fritz.FUNCTION_ALARM })
.then (devices) =>
ains = []
for device in devices
ains.push device.identifier
env.logger.info "Contact sensor AINs: " + ains
# auto discovery
@framework.deviceManager.on('discover', (eventData) =>
@framework.deviceManager.discoverMessage(
'pimatic-fritz', "Scanning DECT devices"
)
@fritzCall("getSwitchList")
.then (ains) =>
for ain in ains
config = {
class: 'FritzOutlet',
id: "switch-" + ain,
ain: ain
}
@framework.deviceManager.discoveredDevice(
'pimatic-fritz', "Switch (#{ain})", config
)
# thermostat list
@fritzCall("getThermostatList")
.then (ains) =>
for ain in ains
config = {
class: 'FritzThermostat',
id: "thermostat-" + ain,
ain: ain
}
@framework.deviceManager.discoveredDevice(
'pimatic-fritz', "Thermostat (#{ain})", config
)
config = {
class: 'FritzTemperatureSensor',
id: "temperature-" + ain,
ain: ain
}
@framework.deviceManager.discoveredDevice(
'pimatic-fritz', "Temperature sensor (#{ain})", config
)
# alarm sensors
@fritzCall("getDeviceListFiltered", { functionbitmask: fritz.FUNCTION_ALARM })
.then (devices) =>
for device in devices
ain = device.identifier
config = {
class: 'FritzContactSensor',
id: "contact-" + ain,
ain: ain
}
@framework.deviceManager.discoveredDevice(
'pimatic-fritz', "Contact sensor (#{ain})", config
)
)
@framework.deviceManager.registerDeviceClass("FritzOutlet", {
configDef: deviceConfigDef.FritzOutlet,
createCallback: (config, lastState) =>
return new FritzOutletDevice(config, lastState, this)
})
@framework.deviceManager.registerDeviceClass("FritzWlan", {
configDef: deviceConfigDef.FritzWlan,
createCallback: (config, lastState) =>
return new FritzWlanDevice(config, lastState, this)
})
@framework.deviceManager.registerDeviceClass("FritzThermostat", {
configDef: deviceConfigDef.FritzThermostat,
createCallback: (config, lastState) =>
return new FritzThermostatDevice(config, lastState, this)
})
@framework.deviceManager.registerDeviceClass("FritzTemperatureSensor", {
configDef: deviceConfigDef.FritzTemperatureSensor,
createCallback: (config, lastState) =>
return new FritzTemperatureSensorDevice(config, lastState, this)
})
@framework.deviceManager.registerDeviceClass("FritzContactSensor", {
configDef: deviceConfigDef.FritzContactSensor,
createCallback: (config, lastState) =>
return new FritzContactSensorDevice(config, lastState, this)
})
# # wait till all plugins are loaded
# @framework.on "after init", =>
# mobileFrontend = @framework.pluginManager.getPlugin 'mobile-frontend'
# if mobileFrontend?
# mobileFrontend.registerAssetFile 'js', "pimatic-fritz/simplethermostat-item.coffee"
# mobileFrontend.registerAssetFile 'html', "pimatic-fritz/simplethermostat-item.jade"
return
# ####fritzCall()
# `fritzCall` can call functions on the smartfritz api and automatically establish session
# @todo: implement network retry
fritzCall: (functionName, args...) =>
# chain calls to the FritzBox to obtain single session id, make sure we have a promise in the first place
return @fritzPromise = (@fritzPromise or Promise.resolve()).reflect().then =>
env.logger.debug "#{functionName} #{@sid} " + (args||[]).join(" ")
return (fritz[functionName] @sid, args..., { url: @config.url })
.catch (error) =>
if error.response?.statusCode == 403
env.logger.warn "Re-establishing session at " + @config.url
return fritz.getSessionID(@config.user, @config.password, { url: @config.url })
.then (@sid) =>
return fritz[functionName] @sid, args..., { url: @config.url } # retry with new sid
.catch =>
# handle sid == '0000000000000000'
throw new Error "Invalid Session-Id: Invalid username or password"
env.logger.error "Cannot access #{error.options?.url}: #{error.response?.statusCode}"
throw error
fritzClampTemperature: (temp) =>
if temp is "on"
return fritz.MAX_TEMP # indicate "high temp"
else if temp is "off"
return fritz.MIN_TEMP # indicate "low temp"
return temp
# ###FritzOutletDevice class
class FritzOutletDevice extends env.devices.SwitchActuator
attributes:
state:
description: "Current state of the outlet"
type: t.boolean
labels: ['on', 'off']
power:
description: "Current power"
type: t.number
unit: 'W'
energy:
description: "Total energy"
type: t.number
unit: 'kWh'
displaySparkline: false
actions:
turnOn:
description: "turns the outlet on"
turnOff:
description: "turns the outlet off"
changeStateTo:
description: "changes the outlet to on or off"
params:
state:
type: t.boolean
toggle:
description: "toggle the state of the outlet"
getState:
description: "returns the current state of the outlet"
returns:
state:
type: t.boolean
# template: 'fritz-outlet'
# status variables
_power: null
_energy: null
_state: null
# Initialize device by reading entity definition from middleware
constructor: (@config, lastState, @plugin) ->
@id = @config.id
@name = @config.name
@interval = 1000 * (@config.interval or @plugin.config.interval)
# keep updating
@requestUpdate()
@intervalTimerID = setInterval( =>
@requestUpdate()
, @interval
)
super()
destroy: () ->
if @intervalTimerID?
clearInterval @intervalTimerID
super()
# poll device according to interval
requestUpdate: ->
@plugin.fritzCall("getSwitchState", @config.ain)
.then (state) =>
@_setState(if state then on else off)
@plugin.fritzCall("getSwitchPower", @config.ain)
.then (power) =>
@_setPower(power)
@plugin.fritzCall("getSwitchEnergy", @config.ain)
.then (energy) =>
@_setEnergy(Math.round(energy / 100.0) / 10.0)
.catch (error) =>
if not error.response?
env.logger.error error.message
# Get current value of last update in defined unit
getPower: -> Promise.resolve(@_power)
_setPower: (power) ->
if @_power is power then return
@_power = power
@emit "power", power
# Get total value of last update in defined unit
getEnergy: -> Promise.resolve(@_energy)
_setEnergy: (energy) ->
if @_energy is energy then return
@_energy = energy
@emit "energy", energy
# Returns a promise that is fulfilled when done.
changeStateTo: (state) ->
@plugin.fritzCall((if state then "setSwitchOn" else "setSwitchOff"), @config.ain)
.then (newState) =>
throw "Could not set switch state" if state != newState
@_setState(if newState then on else off)
Promise.resolve()
# ###FritzWlanDevice class
class FritzWlanDevice extends env.devices.SwitchActuator
attributes:
state:
description: "Current state of the guest wlan"
type: t.boolean
labels: ['on', 'off']
ssid:
description: "SSID"
type: t.string
psk:
description: "PSK"
type: t.string
actions:
turnOn:
description: "turns the guest wlan on"
turnOff:
description: "turns the guest wlan off"
changeStateTo:
description: "changes the guest wlan to on or off"
params:
state:
type: t.boolean
toggle:
description: "toggle the state of the guest wlan"
getState:
description: "returns the current state of the guest wlan"
returns:
state:
type: t.boolean
# status variables
_ssid: null
_psk: null
_state: null
# Initialize device by reading entity definition from middleware
constructor: (@config, lastState, @plugin) ->
@id = @config.id
@name = @config.name
@interval = 1000 * (@config.interval or @plugin.config.interval)
# keep updating
@requestUpdate()
@intervalTimerID = setInterval( =>
@requestUpdate()
, @interval
)
super()
destroy: () ->
if @intervalTimerID?
clearInterval @intervalTimerID
super()
# poll device according to interval
requestUpdate: ->
@plugin.fritzCall("getGuestWlan")
.then (settings) =>
@_setState(if settings.activate_guest_access then on else off)
@_setSsid(settings.guest_ssid)
@_setPsk(settings.wpa_key)
.catch (error) =>
if not error.response?
env.logger.error error.message
# Get current value of last update
getSsid: -> Promise.resolve(@_ssid)
_setSsid: (ssid) ->
if @_ssid is ssid then return
@_ssid = ssid
@emit "ssid", ssid
getPsk: -> Promise.resolve(@_psk)
_setPsk: (psk) ->
if @_psk is psk then return
@_psk = psk
@emit "psk", psk
# Returns a promise that is fulfilled when done.
changeStateTo: (state) ->
@plugin.fritzCall("setGuestWlan", state)
.then (settings) =>
throw "Could not set guest WLAN state" if state != settings.activate_guest_access
@_setState(if settings.activate_guest_access then on else off)
@_setSsid(settings.guest_ssid)
@_setPsk(settings.wpa_key)
Promise.resolve()
# ###FritzThermostat class
# FritzThermostat devices are a hybrif of
# env.devices.HeatingThermostat that they inherit from and
# env.devices.TemperatureSensor that are additionally implemented
class FritzThermostatDevice extends env.devices.HeatingThermostat
# customize HeatingThermostat attributes
attributes:
temperatureSetpoint:
label: "Temperature Setpoint"
description: "The temp that should be set"
type: "number"
discrete: true
unit: "°C"
synced:
description: "Pimatic and thermostat in sync"
type: "boolean"
# implement env.devices.TemperatureSensor
temperature:
description: "The measured temperature"
type: t.number
unit: '°C'
acronym: 'T'
# customize HeatingThermostat actions
actions:
changeTemperatureTo:
params:
temperatureSetpoint:
type: "number"
# implement env.devices.TemperatureSensor
_temperature: null
# Initialize device by reading entity definition from middleware
constructor: (@config, lastState, @plugin) ->
@id = @config.id
@name = @config.name
@interval = 1000 * (@config.interval or @plugin.config.interval)
# initial state
@_temperature = lastState?.temperature?.value
@_temperatureSetpoint = lastState?.temperatureSetpoint?.value
@_synced = true
# implement env.devices.TemperatureSensor
# @attributes = _.extend @attributes, @customAttributes
# get temp settings
@readDefaultTemperatures()
# keep updating
@requestUpdate()
@intervalTimerID = setInterval( =>
@requestUpdate()
, @interval
)
super()
destroy: () ->
if @intervalTimerID?
clearInterval @intervalTimerID
super()
# implement env.devices.HeatingThermostat
changeTemperatureTo: (temperatureSetpoint) ->
@_setSynced(false)
@plugin.fritzCall("setTempTarget", @config.ain, temperatureSetpoint)
.then (temp) =>
throw "Could not set temperature setpoint" if temperatureSetpoint != temp
@_setSetpoint(temperatureSetpoint)
@_setSynced(true)
Promise.resolve()
# implement env.devices.HeatingThermostat
changeModeTo: (mode) ->
# changing modes is not supported.
return Promise.resolve()
# implement env.devices.TemperatureSensor
_setTemperature: (value) ->
if @_temperature is value then return
@_temperature = value
@emit 'temperature', value
# implement env.devices.TemperatureSensor
getTemperature: -> Promise.resolve(@_temperature)
readDefaultTemperatures: ->
@plugin.fritzCall("getTempComfort", @config.ain)
.then (temp) =>
temp = @plugin.fritzClampTemperature temp
@emit "comfyTemp", temp
@plugin.fritzCall("getTempNight", @config.ain)
.then (temp) =>
temp = @plugin.fritzClampTemperature temp
@emit "ecoTemp", temp
@_setSynced(true)
.catch (error) =>
if not error.response?
env.logger.error error.message
# poll device according to interval
requestUpdate: ->
@plugin.fritzCall("getTemperature", @config.ain)
.then (temp) =>
temp = @plugin.fritzClampTemperature temp
@_setTemperature(temp)
@plugin.fritzCall("getTempTarget", @config.ain)
.then (temp) =>
temp = @plugin.fritzClampTemperature temp
@_setSetpoint(temp)
.catch (error) =>
if not error.response?
env.logger.error error.message
# ###FritzTemperatureSensor class
# FritzTemperatureSensor device models the temperature of the Comet DECT thermostats
class FritzTemperatureSensorDevice extends env.devices.TemperatureSensor
# Initialize device by reading entity definition from middleware
constructor: (@config, lastState, @plugin) ->
@id = @config.id
@name = @config.name
@interval = 1000 * (@config.interval or @plugin.config.interval)
# initial state
@_temperature = lastState?.temperature?.value
# keep updating
@requestUpdate()
@intervalTimerID = setInterval( =>
@requestUpdate()
, @interval
)
super()
destroy: () ->
if @intervalTimerID?
clearInterval @intervalTimerID
super()
# poll device according to interval
requestUpdate: ->
@plugin.fritzCall("getTemperature", @config.ain)
.then (temp) =>
temp = @plugin.fritzClampTemperature temp
@_setTemperature(temp)
.catch (error) =>
if not error.response?
env.logger.error error.message
# ###FritzContactSensor class
# FritzContactSensor device models the window open sensors (HAN FUN or DECT)
class FritzContactSensorDevice extends env.devices.ContactSensor
# Initialize device by reading entity definition from middleware
constructor: (@config, lastState, @plugin) ->
@id = @config.id
@name = @config.name
@interval = 1000 * (@config.interval or @plugin.config.interval)
# initial state
@_contact = lastState?.contact?.value
# keep updating
@requestUpdate()
@intervalTimerID = setInterval( =>
@requestUpdate()
, @interval
)
super()
destroy: () ->
if @intervalTimerID?
clearInterval @intervalTimerID
super()
# poll device according to interval
requestUpdate: ->
@plugin.fritzCall("getDeviceListFiltered", { identifier: @config.ain })
.then (devices) =>
if devices[0]?.alert?
@_setContact(1-devices[0].alert.state)
.catch (error) =>
if not error.response?
env.logger.error error.message
# ###Finally
fritzPlugin = new FritzPlugin
return fritzPlugin