diff --git a/fhem/CHANGED b/fhem/CHANGED index e57e0d8cd..4423a05f6 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 59_LuftdatenInfo: introducing new module to fetch PM, temperature + and humidity data from Luftdaten.info - change: 93_DbRep: new version 4.12.1, get tableinfo changed for MySQL - bugfix: 98_DOIFtools: fixed browser issues for Chrome and IE - feature: 98_monitoring: whitelist attribute added diff --git a/fhem/FHEM/59_LuftdatenInfo.pm b/fhem/FHEM/59_LuftdatenInfo.pm new file mode 100644 index 000000000..089ec51c1 --- /dev/null +++ b/fhem/FHEM/59_LuftdatenInfo.pm @@ -0,0 +1,634 @@ +# Id ########################################################################## +# $Id$ + +# copyright ################################################################### +# +# 59_LuftdatenInfo.pm +# +# Copyright by igami +# +# This file is part of FHEM. +# +# FHEM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# FHEM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with FHEM. If not, see . + +# packages #################################################################### +package main; + use strict; + use warnings; + + use Encode qw(decode encode); + + use HttpUtils; + +# forward declarations ######################################################## +sub LuftdatenInfo_Initialize($); + +sub LuftdatenInfo_Define($$); +sub LuftdatenInfo_Undefine($$); +sub LuftdatenInfo_Set($@); +sub LuftdatenInfo_Attr(@); + +sub LuftdatenInfo_GetHttpResponse($$); +sub LuftdatenInfo_ParseHttpResponse($); + +sub LuftdatenInfo_readPassword($); +sub LuftdatenInfo_statusRequest($); +sub LuftdatenInfo_storePassword($$); + +# initialize ################################################################## +sub LuftdatenInfo_Initialize($) { + my ($hash) = @_; + my $TYPE = "LuftdatenInfo"; + + $hash->{DefFn} = $TYPE."_Define"; + $hash->{UndefFn} = $TYPE."_Undefine"; + $hash->{SetFn} = $TYPE."_Set"; + $hash->{AttrFn} = $TYPE."_Attr"; + + $hash->{AttrList} = "" + . "disable:1,0 " + . "disabledForIntervals " + . "interval " + . $readingFnAttributes + ; +} + +# regular Fn ################################################################## +sub LuftdatenInfo_Define($$) { + my ($hash, $def) = @_; + my ($SELF, $TYPE, @id) = split(/[\s]+/, $def); + my $rc = eval{ + require JSON; + JSON->import(); + 1; + }; + + return( + "Error loading JSON. Maybe this module is not installed? " + . "\nUnder debian (based) system it can be installed using " + . "\"apt-get install libjson-perl\"" + ) unless($rc); + + delete($hash->{READINGS}); + delete($hash->{SENSORID1}); + delete($hash->{SENSORID2}); + delete($hash->{ADDRESS}); + + if(looks_like_number($id[0])){ + return("Usage: define $TYPE []") + if(@id != 1 && @id != 2); + + $id[1] = $id[0] + 1 unless($id[1]); + + $hash->{SENSORID1} = $id[0]; + $hash->{SENSORID2} = $id[1]; + $hash->{CONNECTION} = "remote"; + } + else{ + return("Usage: define $TYPE ") + if(@id != 1); + + $hash->{ADDRESS} = $id[0]; + $hash->{CONNECTION} = "local"; + } + + my $minInterval = 300; + my $interval = AttrVal($SELF, "interval", $minInterval); + $interval = $minInterval unless(looks_like_number($interval)); + $interval = $minInterval if($interval < $minInterval); + + $hash->{INTERVAL} = $interval; + + readingsSingleUpdate($hash, "state", "active", 1); + + LuftdatenInfo_statusRequest($hash); + + return; +} + +sub LuftdatenInfo_Undefine($$) { + my ($hash, $arg) = @_; + + HttpUtils_Close($hash); + RemoveInternalTimer($hash); + + return; +} + +sub LuftdatenInfo_Set($@) { + my ($hash, @a) = @_; + my $TYPE = $hash->{TYPE}; + + return "\"set $TYPE\" needs at least one argument" if(@a < 2); + + my $SELF = shift @a; + my $argument = shift @a; + my $value = join(" ", @a) if (@a); + + my %LuftdatenInfo_sets = ( + "statusRequest" => "statusRequest:noArg", + ); + + return( + "Unknown argument $argument, choose one of " + . join(" ", values %LuftdatenInfo_sets) + ) if(!exists($LuftdatenInfo_sets{$argument})); + + if(!IsDisabled($SELF)){ + if($argument eq "statusRequest"){ + LuftdatenInfo_statusRequest($hash); + } + } + + return; +} + +sub LuftdatenInfo_Attr(@) { + my ($cmd, $SELF, $attribute, $value) = @_; + my $hash = $defs{$SELF}; + my $TYPE = $hash->{TYPE}; + + Log3($SELF, 5, "$TYPE ($SELF) - entering LuftdatenInfo_Attr"); + + if($attribute eq "disable"){ + if($value && $value == 1){ + readingsSingleUpdate($hash, "state", "disabled", 1); + } + elsif($cmd eq "del" || !$value){ + LuftdatenInfo_statusRequest($hash); + + readingsSingleUpdate($hash, "state", "active", 1); + } + } + elsif($attribute eq "interval"){ + my $minInterval = 300; + my $interval = $cmd eq "set" ? $value : $minInterval; + $interval = $minInterval unless(looks_like_number($interval)); + $interval = $minInterval if($interval < $minInterval); + + $hash->{INTERVAL} = $interval; + } + + return; +} + +# HttpUtils Fn ################################################################ +sub LuftdatenInfo_GetHttpResponse($$) { + my ($hash, $id) = @_; + my $SELF = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $timeout = 5; + my $connection = $hash->{CONNECTION}; + + Log3($SELF, 5, "$TYPE ($SELF) - entering LuftdatenInfo_GetHttpResponse"); + + if($connection eq "remote"){ + my $param = { + url => "http://api.luftdaten.info/v1/sensor/$id", + timeout => $timeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&LuftdatenInfo_ParseHttpResponse, + }; + + HttpUtils_NonblockingGet($param); + } + elsif($connection eq "local"){ + my $param = { + url => "http://".$id."/data.json", + timeout => $timeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&LuftdatenInfo_ParseHttpResponse, + }; + + HttpUtils_NonblockingGet($param); + } +} + +sub LuftdatenInfo_ParseHttpResponse($) { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $SELF = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $connection = $hash->{CONNECTION}; + + Log3($SELF, 5, "$TYPE ($SELF) - entering LuftdatenInfo_ParseHttpResponse"); + + if($err ne ""){ + Log3($SELF, 2, "$TYPE ($SELF) - error while request: $err"); + + readingsSingleUpdate($hash, "state", "error", 1); + } + elsif($data eq "[]"){ + if(index($param->{url}, $hash->{SENSORID2}) > -1){ + delete($hash->{SENSORID2}); + + Log3($SELF, 2, "$TYPE ($SELF) - no second sensor found"); + } + else{ + Log3($SELF, 2, "$TYPE ($SELF) - no data returned"); + + readingsSingleUpdate($hash, "state", "no data", 1); + } + } + elsif($data ne ""){ + Log3 $SELF, 4, "$TYPE ($SELF) - returned data: $data"; + + $data = encode('UTF-8', $data); + $data = JSON->new->utf8->decode($data); + + if($param->{url} =~ m/openstreetmap/){ + my $address = $data->{address}; + + readingsSingleUpdate( + $hash, "location", "$address->{postcode} $address->{city}", 1 + ); + } + elsif($connection eq "remote"){ + my $sensor = @{$data}[-1]; + my $sensor_type = $sensor->{sensor}{sensor_type}{name}; + my $timestamp = $sensor->{timestamp}; + + return unless($timestamp ge ReadingsVal($SELF, ".timestamp", "")); + + Log3 $SELF, 5, "$TYPE ($SELF) - returned data is newer than readings"; + + if($sensor_type eq "SDS011"){ + Log3 $SELF, 5, "$TYPE ($SELF) - parsing $sensor_type data"; + + my $latitude = $sensor->{location}{latitude}; + my $longitude = $sensor->{location}{longitude}; + + unless(ReadingsVal($SELF, "location", undef)){ + my $param = { + url => + "http://nominatim.openstreetmap.org/reverse?" + . "format=json&lat=$latitude&lon=$longitude" + , + timeout => 5, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&LuftdatenInfo_ParseHttpResponse, + }; + + HttpUtils_NonblockingGet($param); + } + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, ".timestamp", $timestamp); + + foreach (@{$sensor->{sensordatavalues}}){ + if($_->{value_type} eq "P1"){ + readingsBulkUpdate($hash, "PM10", $_->{value}); + } + elsif($_->{value_type} eq "P2"){ + readingsBulkUpdate($hash, "PM2.5", $_->{value}); + } + } + + readingsBulkUpdateIfChanged($hash, "latitude", $latitude); readingsBulkUpdateIfChanged($hash, "longitude", $longitude); readingsBulkUpdate($hash, "state", "active"); + readingsEndUpdate($hash, 1); + + my $SENSORID2 = InternalVal($SELF, "SENSORID2", undef); + + LuftdatenInfo_GetHttpResponse($hash, $SENSORID2) + if(defined($SENSORID2)); + } + elsif($sensor_type eq "DHT22"){ + Log3 $SELF, 5, "$TYPE ($SELF) - parsing $sensor_type data"; + + if( + $sensor->{location}{latitude} ne + ReadingsVal($SELF, "latitude", "") + || $sensor->{location}{longitude} ne + ReadingsVal($SELF, "longitude", "") + ){ + delete($hash->{SENSORID2}); + + Log3( + $SELF, 2 + , "$TYPE ($SELF) - DHT22 position differs from SDS011 position" + ); + + return; + } + else{ + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, ".timestamp", $timestamp); + + foreach (@{$sensor->{sensordatavalues}}){ + $_->{value} =~ m/^(\S+)(\s|$)/; + + if($_->{value_type} eq "temperature"){ + readingsBulkUpdate($hash, "temperature", $1); + } + elsif($_->{value_type} eq "humidity"){ + readingsBulkUpdate($hash, "humidity", $1); + } + } + + readingsBulkUpdate($hash, "state", "active"); + readingsEndUpdate($hash, 1); + } + } + } + elsif($connection eq "local"){ + readingsBeginUpdate($hash); + + foreach (@{$data->{sensordatavalues}}){ + $_->{value} =~ m/^(\S+)(\s|$)/; + + if($_->{value_type} eq "temperature"){ + readingsBulkUpdate($hash, "temperature", $1); + } + elsif($_->{value_type} eq "humidity"){ + readingsBulkUpdate($hash, "humidity", $1); + } + elsif($_->{value_type} eq "SDS_P1"){ + readingsBulkUpdate($hash, "PM10", $1); + } + elsif($_->{value_type} eq "SDS_P2"){ + readingsBulkUpdate($hash, "PM2.5", $1); + } + elsif($_->{value_type} eq "signal"){ + readingsBulkUpdate($hash, "signal", $1); + } + } + readingsEndUpdate($hash, 1); + } + } + + return; +} + +# module Fn ################################################################### +sub LuftdatenInfo_statusRequest($) { + my ($hash) = @_; + my $SELF = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $interval = $hash->{INTERVAL}; + my $connection = $hash->{CONNECTION}; + + Log3($SELF, 5, "$TYPE ($SELF) - entering LuftdatenInfo_statusRequest"); + + RemoveInternalTimer($hash); + + return if(IsDisabled($SELF)); + + InternalTimer( + gettimeofday() + $interval, "LuftdatenInfo_statusRequest", $hash + ); + + if($connection eq "remote"){ + LuftdatenInfo_GetHttpResponse($hash, $hash->{SENSORID1}); + } + elsif($connection eq "local"){ + LuftdatenInfo_GetHttpResponse($hash, $hash->{ADDRESS}); + } + + return; +} + +1; + +# commandref ################################################################## +=pod +=item summary provides data from Luftdaten.info +=item summary_DE stellt Daten von Luftdaten.info bereit + +=begin html + + +

LuftdatenInfo

+(en | de) +
+
    + LuftdatenInfo is the FHEM module to read particulate matter, temperature and humidity values ​​from the self-assembly particulate matter sensors from Luftdaten.info.
    + The values ​​can be queried directly from the server or locally.
    + A local query should only be made if the sensor is NOT sendig data to the server, otherwise the sensor may block and need to be restarted.
    +
    + Prerequisites +
      + The Perl module "JSON" is required.
      + Under Debian (based) system, this can be installed using + "apt-get install libjson-perl". +
    +
    + + Define +
      + + define <name> LuftdatenInfo + (<SDS011sensorID> [<DHT22sensorID>]|<ip>) +
      + To query the data from the server, the SDS011 SensorID must be specified.
      + The SensorID stands right at + + http://maps.luftdaten.info/ + + . The DHT22 SensorID is usually the SDS011 SensorID + 1 and does not have + to be specified explicitly. While parsing the data the location values + from both sensors will be compared and a message will be written into the + log if they differ.
      + For a local query of the data, the IP address or hostname must be + specified. +

    + + Set +
      +
    • + statusRequest
      + Starts a status request. +
    • +

    + + Readings
    +
      +
    • + PM10
      + Quantity of particles with a diameter of less than 10 μm in μg / m³ +
    • +
    • + PM2.5
      + Quantity of particles with a diameter of less than 2.5 μm in μg / m³ +
    • +
    • + temperature
      + Temperature in °C +
    • +
    • + humidity
      + Relative humidity in% +
    • +
    • + latitude
      + latitude
      + Only available with remote query. +
    • +
    • + location
      + location as "postcode city"
      + Only available with remote query. +
    • +
    • + longitude
      + longitude
      + Only available with remote query. +
    • +
    • + signal
      + WLAN signal strength in dBm
      + Only available with local query. +
    • +

    + + Attribute + +
+
+ +=end html + +=begin html_DE + + +

LuftdatenInfo

+(en | de) +
+
    + LuftdatenInfo ist das FHEM Modul um Feinstaub-, Temperatur- und + Luftfeuchtichkeitswerte von den selbstbau Feinstaub Sensoren von + Luftdaten.info auszulesen.
    + Dabei können die Werte direkt vom Server oder auch lokal abgefragt + werden.
    + Eine lokale Abfrage sollte nur erfolgen, wenn der Sensor NICHT an den + Server sendet, sonst kann es passieren, dass der Sensor blockiert und + neugestartet werden muss.
    +
    + Vorraussetzungen +
      + Das Perl-Modul "JSON" wird benötigt.
      + Unter Debian (basierten) System, kann dies mittels + "apt-get install libjson-perl" installiert werden. +
    +
    + + Define +
      + + define <name> LuftdatenInfo + (<SDS011sensorID> [<DHT22sensorID>]|<ip>) +
      + Für eine Abfrage der Daten vom Server muss die SensorID von dem + SDS011 Sensor angegeben werden. Diese steht rechts auf der Seite + + http://maps.luftdaten.info/ + + . Die DHT22 SensorID entspricht normalerweise der SDS011 SensorID + 1 und + muss nicht explizit mit angegeben werden. Bei einer Abfrage werden die + beiden Positionsangaben verglichen und bei Abweichung eine Meldung ins + Log geschrieben.
      + Für eine lokale Abfrage der Daten muss die IP Addresse oder der + Hostname angegeben werden. +

    + + Set +
      +
    • + statusRequest
      + Startet eine Abfrage der Daten. +
    • +

    + + Readings
    +
      +
    • + PM10
      + Menge der Partikel mit einem Durchmesser von weniger als 10 µm in µg/m³ +
    • +
    • + PM2.5
      + Menge der Partikel mit einem Durchmesser von weniger als 2.5 µm in µg/m³ +
    • +
    • + temperature
      + Temperatur in °C +
    • +
    • + humidity
      + Relative Luftfeuchtgkeit in % +
    • +
    • + latitude
      + Breitengrad
      + Nur bei remote Abfrage verfügbar. +
    • +
    • + location
      + Standort als "Postleitzahl Ort"
      + Nur bei remote Abfrage verfügbar. +
    • +
    • + longitude
      + Längengrad
      + Nur bei remote Abfrage verfügbar. +
    • +
    • + signal
      + WLAN Signalstärke in dBm
      + Nur bei local Abfrage verfügbar. +
    • +

    + + Attribute + +
+
+ +=end html_DE +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 230d9b62c..45a1067b1 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -245,6 +245,7 @@ FHEM/56_POKEYS.pm axelberner http://forum.fhem.de Sonstiges FHEM/57_Calendar.pm neubert http://forum.fhem.de Unterstuetzende Dienste/Kalendermodule FHEM/57_CALVIEW.pm chris1284 http://forum.fhem.de Unterstuetzende Dienste/Kalendermodule FHEM/59_HCS.pm mfr69bs http://forum.fhem.de Automatisierung +FHEM/59_LuftdatenInfo igami http://forum.fhem.de Bastelecke FHEM/59_OPENWEATHER.pm tupol http://forum.fhem.de Unterstuetzende Dienste/Wettermodule (Link als PM an tupol) FHEM/59_Twilight.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste/Wettermodule FHEM/59_PROPLANTA.pm tupol http://forum.fhem.de Unterstuetzende Dienste/Wettermodule (Link als PM an tupol)