pimatic-luftdaten
Version:
Pimatic plugin for luftdaten sensors
463 lines (420 loc) • 17.4 kB
text/coffeescript
module.exports = (env) ->
rp = require 'request-promise'
calc = require './lib/calc.js'
_ = env.require 'lodash'
class Luftdaten extends env.plugins.Plugin
init: (app, , ) =>
deviceConfigDef = require("./device-config-schema")
.deviceManager.registerDeviceClass("LuftdatenDevice", {
configDef: deviceConfigDef.LuftdatenDevice,
createCallback: (config) -> new LuftdatenDevice(config)
})
.deviceManager.registerDeviceClass("LuftdatenHomeDevice", {
configDef: deviceConfigDef.LuftdatenHomeDevice,
createCallback: (config) -> new LuftdatenHomeDevice(config)
})
class LuftdatenDevice extends env.devices.Device
attributesTemplate:
SENSOR_ID:
description: "The ID(s) of the sensor(s)"
type: "string"
unit: ''
acronym: 'sensor'
DISTANCE:
description: "The distance of the sensor in km"
type: "number"
unit: 'km'
acronym: 'distance'
PM10:
description: "The PM10 air quality"
type: "number"
unit: 'ug/m3'
acronym: 'PM10'
PM25:
description: "The PM2.5 air quality"
type: "number"
unit: 'ug/m3'
acronym: 'pm2.5'
TEMP:
description: "The temperature"
type: "number"
unit: '°C'
acronym: 'temp'
HUM:
description: "The humidity"
type: "number"
unit: '%'
acronym: 'hum'
BAR:
description: "The air pressure"
type: "number"
unit: 'hPa'
acronym: 'bar'
BAR_SEA:
description: "The air pressure"
type: "number"
unit: 'hPa'
acronym: 'barsea'
WIFI:
description: "The Wifi signal strength"
type: "number"
unit: 'dBm'
acronym: 'wifi'
NOISE_LEVEL:
description: "Noise level"
type: "string"
unit: ''
acronym: 'sounds'
NOISE_LEQ:
description: "Noise"
type: "number"
unit: 'dBA'
acronym: 'noise'
NOISE_LMIN:
description: "noiseLm"
type: "number"
unit: 'dBA'
acronym: 'noiseLm'
NOISE_LMAX:
description: "Noise"
type: "number"
unit: 'dBA'
acronym: 'noiseLM'
AQI:
description: "The Air Quality Index"
type: "number"
unit: '/500'
acronym: 'aqi'
AQI_CODE:
description: "The Air Quality Index code"
type: "string"
unit: ''
acronym: 'aqiCode'
AQI_AIR_QUALITY:
description: "The Air Quality Description"
type: "string"
unit: ''
acronym: 'airQuality'
constructor: () ->
= .id
= .name
= if ?.sensorId? and ?.sensorId isnt "" then .sensorId else null
= if ?.latitude? and ?.latitude isnt "" then .latitude else null
= if ?.longitude? and ?.longitude isnt "" then .longitude else null
= if ?.radius? or ?.radius is "" then .radius else 1
= 0
if is null and ( is null or is null)
throw new Error("No sensor configured")
= _.cloneDeep()
= {}
= if ? then .replace(/\s+/g,'')
=
single: 1
multi: 2
area: 3
local: 4
= null
= "https://api.luftdaten.info/v1/sensor/#{@sensorId}/"
= "https://api.luftdaten.info/v1/filter/area=#{@latitude},#{@longitude},#{@radius}"
= "http://#{@sensorId}/data.json"
= null
= .interval * 60000 # Check for changes every interval in minutes
= 50 # km
if >
=
env.logger.info "Radius is too large and is set to maximum = " + + " km"
if ?
if Number.isInteger(Number )
=
= .single
else if .match(
"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])$")
=
= .local
else if ? and ?
=
= .area
if not ? then throw new Error("Not a valid sensorID, Lat/Lon coordinates or local IP adress")
= {}
.PM10 = lastState?.PM10?.value or 0.0
.PM25 = lastState?.PM25?.value or 0.0
.TEMP = lastState?.TEMP?.value or 0.0
.HUM = lastState?.HUM?.value or 0.0
.BAR = lastState?.BAR?.value or 0.0
.BAR_SEA = lastState?.BAR_SEA?.value or 0.0
.WIFI = lastState?.WIFI?.value or 0
.NOISE_LEVEL = lastState?.NOISE_LEVEL?.value or ""
.NOISE_LEQ = lastState?.NOISE_LEQ?.value or 0.0
.NOISE_LMIN = lastState?.NOISE_LMIN?.value or 0.0
.NOISE_LMAX = lastState?.NOISE_LMAX?.value or 0.0
.AQI = lastState?.AQI?.value or 0
.AQI_CODE = lastState?.AQI_CODE?.value or 0
.AQI_AIR_QUALITY = lastState?.AQI_AIR_QUALITY?.value or 0
.DISTANCE = lastState?.DISTANCE?.value or 0
.SENSOR_ID = lastState?.SENSOR_ID?.value or 0
for attribute in .attributes
do (attribute) =>
[attribute.name] =
description: [attribute.name].description
type: [attribute.name].type
unit: if [attribute.name].unit? then [attribute.name].unit else ""
label: if [attribute.name].label? then [attribute.name].label else attribute.name
acronym: if [attribute.name].acronym? then [attribute.name].acronym else attribute.name
attribute.name, () =>
return Promise.resolve [attribute.name]
super()
destroy: () ->
.cancel() if ?
clearTimeout if ?
super()
requestData: () =>
= rp()
.then((data) =>
d = JSON.parse(data)
= {}
if Array.isArray(d)
= d[0]
= Math.min(, )
for _record in d
if is .single # or is .multi#?
#check if most recent record is used
for _values in _record.sensordatavalues
[_values.value_type] =
value_type: _values.value_type
id: _record.sensor.id
#check if most recent record is used
if new Date(_record.timestamp) >= new Date(.timestamp)
.SENSOR_ID = _record.sensor.id
= _record
if is .area # isnt null and isnt null
=
.DISTANCE =
else if is .area # isnt null and isnt null
# search outside in for closest sensors to get full data
=
if <=
=
.DISTANCE =
for _val in _record.sensordatavalues
# update data will closer values
[_val.value_type] =
value_type: _val.value_type
id: _record.sensor.id
if _val.value_type in .sensordatavalues
#update value of existing value_type
.sensordatavalues[_val.value_type].value = String _record.sensordatavalues[_val.value_type].value
else
#add closer missing value_type, value and id
env.logger.debug _val.value_type + " added to sensor data, sensorID: " + _record.sensor.id
.sensordatavalues[_val.value_type] =
value_type: _val.value_type
value: String _val.value
id: _val.id
else
= d
= []
for sensor, val of
unless val.id in
.push val.id
.SENSOR_ID =
if not ?
env.logger.debug "no data from " +
return
for k, val of .sensordatavalues
if (val.value_type).match("P1")
.PM10 = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("P2")
.PM25 = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("temperature")
.TEMP = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("humidity")
.HUM = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("pressure")
.BAR = Number(Math.round(val.value/100+'e1')+'e-1')
if (val.value_type).match("pressure_at_sealevel")
.BAR_SEA = Number(Math.round(val.value/100+'e1')+'e-1')
if (val.value_type).match("signal")
.WIFI = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("noise_LAeq")
.NOISE_LEQ = Number(Math.round(val.value+'e1')+'e-1')
.NOISE_LEVEL = calc.dba_label(.NOISE_LEQ)
if (val.value_type).match("noise_LA_min")
.NOISE_LMIN = Number(Math.round(val.value+'e1')+'e-1')
if (val.value_type).match("noise_LA_max")
.NOISE_LMAX = Number(Math.round(val.value+'e1')+'e-1')
lAqi = Math.max(calc.pm10(.PM10), calc.pm25(.PM25))
lAqi = Math.min(lAqi, 500)
lAqi = Math.max(lAqi, 0)
.AQI = lAqi
.AQI_CODE = calc.aqi_color(lAqi)
.AQI_AIR_QUALITY = calc.aqi_label(lAqi)
for _attr of
_attr, [_attr]
= Promise.resolve()
= 0
)
.catch((err) =>
if err instanceof String
if err.indexOf('ETIMEDOUT') >= 0
+=1
if > 4
for _attr of
[_attr] = 0
_attr, [_attr]
= 0
env.logger.error("Luftdaten server is not responding")
)
= unless ?
= setTimeout(, )
return
_distance: (lat1, lon1, lat2, lon2) ->
R = 6371
# Radius of the earth in km
dLat =
# deg2rad below
dLon =
a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos() * Math.cos() * Math.sin(dLon / 2) * Math.sin(dLon / 2)
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
d = R * c
# Distance in km
return Number d
_deg2rad: (deg) ->
return deg * Math.PI / 180
class LuftdatenHomeDevice extends env.devices.Device
attributes:
PM10:
description: "The PM10 air quality"
type: "number"
unit: 'ug/m3'
acronym: 'pm10'
PM25:
description: "The PM2.5 air quality"
type: "number"
unit: 'ug/m3'
acronym: 'pm2.5'
TEMP:
description: "The temperature"
type: "number"
unit: '°C'
acronym: 'temp'
HUM:
description: "The humidity"
type: "number"
unit: '%'
acronym: 'hum'
BAR:
description: "The air pressure"
type: "number"
unit: 'hPa'
acronym: 'bar'
WIFI:
description: "The Wifi signal strength"
type: "number"
unit: 'dBm'
acronym: 'wifi'
AQI:
description: "The Air Quality Index"
type: "number"
unit: '/500'
acronym: 'aqi'
AQI_CODE:
description: "The Air Quality Index Code"
type: "string"
unit: ''
acronym: 'aqiCode'
AQI_AIR_QUALITY:
description: "The Air Quality Description"
type: "string"
unit: ''
acronym: 'airQuality'
constructor: () ->
= .id
= .name
= .sensorIp
= "http://#{@sensorIp}/data.json"
= .interval * 60000 # Check for changes every interval in minutes
= 0
= = = = = = 0
super()
destroy: () ->
.cancel() if ?
clearTimeout if ?
super()
requestData: () =>
= rp()
.then((data) =>
d = JSON.parse(data)
#reset all values
= = = = = = 0
for k, val of d.sensordatavalues
if (val.value_type).match("P1")
= Number(Math.round(val.value+'e1')+'e-1') #PM10
if (val.value_type).match("P2")
= Number(Math.round(val.value+'e1')+'e-1') #PM2.5
if (val.value_type).match("temperature")
= Number(Math.round(val.value+'e1')+'e-1') #Temperature
if (val.value_type).match("humidity")
= Number(Math.round(val.value+'e1')+'e-1') #Humidity
if (val.value_type).match("signal")
= Number(Math.round(val.value+'e1')+'e-1') #Signal
if (val.value_type).match("pressure")
= Number(Math.round(val.value/100+'e1')+'e-1') #Pressure
lAqi = Math.max(calc.pm10(), calc.pm25())
lAqi = Math.min(lAqi, 500)
lAqi = Math.max(lAqi, 0)
"PM10",
"PM25",
"TEMP",
"HUM",
"BAR",
"WIFI",
"AQI", lAqi
"AQI_CODE", calc.aqi_color(lAqi)
"AQI_AIR_QUALITY", calc.aqi_label(lAqi)
= Promise.resolve()
)
.catch((err) =>
if err instanceof String
if err.indexOf('ETIMEDOUT') >= 0
+=1
if > 4
"PM10", 0
"PM25", 0
"AQI", 0
"AQI_CODE", "Unknown"
"AQI_AIR_QUALITY", "Unknown"
= 0
env.logger.error("Luftdaten server is not responding")
)
= unless ?
= setTimeout(, )
return
_setAttribute: (attributeName, value, discrete = false) ->
if not discrete or @[attributeName] isnt value
@[attributeName] = value
attributeName, value
getPM10: ->
.then(=> )
getPM25: ->
.then(=> )
getTEMP: ->
.then(=> )
getHUM: ->
.then(=> )
getBAR: ->
.then(=> )
getWIFI: ->
.then(=> )
getAQI: ->
.then(=> )
getAQI_CODE: ->
.then(=> )
getAQI_AIR_QUALITY: ->
.then(=> )
plugin = new Luftdaten
return plugin