2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-01 07:19:24 +00:00

00_ElsnerWS, 98_ModbusElsnerWS: new modules for Elsner Weatherstations (RS485, Modbus)

git-svn-id: https://svn.fhem.de/fhem/trunk@19414 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
klaus.schauer 2019-05-20 04:16:02 +00:00
parent 73eb9bcf37
commit f041c0ef68
2 changed files with 1635 additions and 0 deletions

868
fhem/FHEM/00_ElsnerWS.pm Normal file
View File

@ -0,0 +1,868 @@
# $Id$
# This modules handles the communication with a Elsner Weather Station P03/3-R485 or P03/4-RS485.
# define: define WS ElsnerWS comtype=rs485 devicename=/dev/ttyUSB1@19200
package main;
use strict;
use warnings;
use Time::HiRes qw(gettimeofday usleep);
if( $^O =~ /Win/ ) {
require Win32::SerialPort;
} else {
require Device::SerialPort;
}
sub ElsnerWS_Checksum($$);
sub ElsnerWS_Define($$$);
sub ElsnerWS_Delete($$);
sub ElsnerWS_Initialize($);
sub ElsnerWS_Read($);
sub ElsnerWS_Ready($);
# Init
sub ElsnerWS_Initialize($) {
my ($hash) = @_;
require "$attr{global}{modpath}/FHEM/DevIo.pm";
# Provider
$hash->{ReadFn} = "ElsnerWS_Read";
$hash->{ReadyFn} = "ElsnerWS_Ready";
# Normal devices
$hash->{DefFn} = "ElsnerWS_Define";
$hash->{UndefFn} = "ElsnerWS_Undef";
$hash->{DeleteFn} = "ElsnerWS_Delete";
$hash->{NotifyFn} = "ElsnerWS_Notify";
$hash->{AttrFn} = "ElsnerWS_Attr";
$hash->{AttrList} = "brightnessDayNight brightnessDayNightCtrl brightnessDayNightDelay " .
"brightnessSunny brightnessSunnySouth brightnessSunnyWest brightnessSunnyEast " .
"brightnessSunnyDelay brightnessSunnySouthDelay brightnessSunnyWestDelay brightnessSunnyEastDelay " .
"timeEvent:no,yes updateGlobalAttr:no,yes " .
"windSpeedWindy windSpeedStormy windSpeedWindyDelay windSpeedStormyDelay " .
$readingFnAttributes;
$hash->{parseParams} = 1;
#$hash->{NotifyOrderPrefix} = "45-";
return;
}
# Define
sub ElsnerWS_Define($$$) {
my ($hash, $a, $h) = @_;
my $name = $a->[0];
#return "ElsnerWS: wrong syntax, correct is: define <name> ElsnerWS {devicename[\@baudrate]|ip:port}" if($#$a != 2);
if (defined $a->[2]) {
$hash->{ComType} = $a->[2];
} elsif (exists $h->{comtype}) {
$hash->{ComType} = $h->{comtype};
} else {
return "ElsnerWS: wrong syntax, correct is: define <name> ElsnerWS [comtype=](rs485) [devicename=]{devicename[\@baudrate]|ip:port}";
}
if ($hash->{ComType} ne 'rs485') {
return "ElsnerWS: wrong syntax, correct is: define <name> ElsnerWS [comtype=](rs485) [devicename=]{devicename[\@baudrate]|ip:port}";
}
if (defined $a->[3]) {
$hash->{DeviceName} = $a->[3];
} elsif (exists $h->{devicename}) {
$hash->{DeviceName} = $h->{devicename};
} else {
return "ElsnerWS: wrong syntax, correct is: define <name> ElsnerWS [comtype=](rs485|modbus) [devicename=]{devicename[\@baudrate]|ip:port}";
}
$hash->{NOTIFYDEV} = "global";
my ($autocreateFilelog, $autocreateHash, $autocreateName, $autocreateDeviceRoom, $autocreateWeblinkRoom) =
('./log/' . $name . '-%Y-%m.log', undef, 'autocreate', 'ElsnerWS', 'Plots');
my ($cmd, $filelogName, $gplot, $ret, $weblinkName, $weblinkHash) =
(undef, "FileLog_$name", "ElsnerWS:SunIntensity,ElsnerWS_2:Temperature/Brightness,ElsnerWS_3:WindSpeed/Raining,", undef, undef, undef);
DevIo_CloseDev($hash);
$ret = DevIo_OpenDev($hash, 0, undef);
# find autocreate device
while (($autocreateName, $autocreateHash) = each(%defs)) {
last if ($defs{$autocreateName}{TYPE} eq "autocreate");
}
$autocreateDeviceRoom = AttrVal($autocreateName, "device_room", $autocreateDeviceRoom) if (defined $autocreateName);
$autocreateDeviceRoom = 'ElsnerWS' if ($autocreateDeviceRoom eq '%TYPE');
$autocreateDeviceRoom = $name if ($autocreateDeviceRoom eq '%NAME');
$autocreateDeviceRoom = AttrVal($name, "room", $autocreateDeviceRoom);
$attr{$name}{room} = $autocreateDeviceRoom if (!exists $attr{$name}{room});
if ($init_done) {
Log3 $name, 2, "ElsnerWS define " . join(' ', @$a);
if (!defined(AttrVal($autocreateName, "disable", undef)) && !exists($defs{$filelogName})) {
# create FileLog
$autocreateFilelog = $attr{$autocreateName}{filelog} if (exists $attr{$autocreateName}{filelog});
$autocreateFilelog =~ s/%NAME/$name/g;
$cmd = "$filelogName FileLog $autocreateFilelog $name";
$ret = CommandDefine(undef, $cmd);
if($ret) {
Log3 $filelogName, 2, "ElsnerWS ERROR: $ret";
} else {
$attr{$filelogName}{room} = $autocreateDeviceRoom;
$attr{$filelogName}{logtype} = 'text';
Log3 $filelogName, 2, "ElsnerWS define $cmd";
}
}
if (!defined(AttrVal($autocreateName, "disable", undef)) && exists($defs{$filelogName})) {
# create FileLog
# add GPLOT parameters
$attr{$filelogName}{logtype} = $gplot . $attr{$filelogName}{logtype}
if (!exists($attr{$filelogName}{logtype}) || $attr{$filelogName}{logtype} eq 'text');
if (AttrVal($autocreateName, "weblink", 1)) {
$autocreateWeblinkRoom = $attr{$autocreateName}{weblink_room} if (exists $attr{$autocreateName}{weblink_room});
$autocreateWeblinkRoom = 'ElsnerWS' if ($autocreateWeblinkRoom eq '%TYPE');
$autocreateWeblinkRoom = $name if ($autocreateWeblinkRoom eq '%NAME');
$autocreateWeblinkRoom = $attr{$name}{room} if (exists $attr{$name}{room});
my $wnr = 1;
#create SVG devices
foreach my $wdef (split(/,/, $gplot)) {
next if(!$wdef);
my ($gplotfile, $stuff) = split(/:/, $wdef);
next if(!$gplotfile);
$weblinkName = "SVG_$name";
$weblinkName .= "_$wnr" if($wnr > 1);
$wnr++;
next if (exists $defs{$weblinkName});
$cmd = "$weblinkName SVG $filelogName:$gplotfile:CURRENT";
Log3 $weblinkName, 2, "ElsnerWS define $cmd";
$ret = CommandDefine(undef, $cmd);
if($ret) {
Log3 $weblinkName, 2, "ElsnerWS ERROR: define $cmd: $ret";
last;
}
$attr{$weblinkName}{room} = $autocreateWeblinkRoom;
$attr{$weblinkName}{title} = '"' . $name . ' Min $data{min1}, Max $data{max1}, Last $data{currval1}"';
$ret = CommandSet(undef, "$weblinkName copyGplotFile");
if($ret) {
Log3 $weblinkName, 2, "ElsnerWS ERROR: set $weblinkName copyGplotFile: $ret";
last;
}
}
}
}
}
return $ret;
}
# Initialize serial communication
sub ElsnerWS_InitSerialCom($) {
# return if attribute list is incomplete
#return undef if (!$init_done);
my ($hash) = @_;
my $name = $hash->{NAME};
if ($hash->{STATE} eq "disconnected") {
Log3 $name, 2, "ElsnerWS $name not initialized";
return undef;
}
$hash->{PARTIAL} = '';
readingsSingleUpdate($hash, "state", "initialized", 1);
Log3 $name, 2, "ElsnerWS $name initialized";
return undef;
}
sub ElsnerWS_Checksum($$) {
my ($packetType, $msg) = @_;
$msg = $packetType . $msg;
my $ml = length($msg);
my $sum = 0;
for(my $i = 0; $i < $ml; $i += 2) {
$sum += hex(substr($msg, $i, 2));
}
return $sum;
}
# Read
# called from the global loop, when the select for hash->{FD} reports data
sub ElsnerWS_Read($) {
my ($hash) = @_;
my $buf = DevIo_SimpleRead($hash);
return "" if(!defined($buf));
my $name = $hash->{NAME};
my $data = $hash->{PARTIAL} . uc(unpack('H*', $buf));
my $averageOrder = 10;
#Log3 $name, 5, "ElsnerWS $name received DATA: " . uc(unpack('H*', $buf));
Log3 $name, 5, "ElsnerWS $name received DATA: $data";
while($data =~ m/^(57)(2B|2D)(.{36})(.{8})(03)(.*)/ ||
$data =~ m/^(57)(2B|2D)(.{66})(.{8})(03)(.*)/ ||
$data =~ m/^(47)(2B|2D)(.{108})(.{8})(03)(.*)/) {
my ($packetType, $ldata, $checksum, $etx, $rest) = ($1, $2 . $3, pack('H*', $4), hex($5), $6);
# data telegram incomplete
last if(!defined $etx);
Log3 $name, 5, "ElsnerWS $name received $packetType, $ldata, $checksum, $etx";
last if($etx != 3);
my $tlen = length($ldata);
$data = $ldata;
my $checksumCalc = ElsnerWS_Checksum($packetType, $data);
if($checksum != $checksumCalc) {
Log3 $name, 2, "ElsnerWS $name wrong checksum: got $checksum, computed $checksumCalc" ;
$data = $rest;
next;
}
$data =~ m/^(..)(........)(....)(....)(....)(..)(......)(........)(..)(.*)/;
my ($temperatureSign, $temperature, $sunSouth, $sunWest, $sunEast, $twilightFlag, $brightness, $windSpeed, $isRaining, $zdata) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
$sunSouth = pack('H*', $sunSouth) * 1000;
$sunWest = pack('H*', $sunWest) * 1000;
$sunEast = pack('H*', $sunEast) * 1000;
$brightness = pack('H*', $brightness);
my @sunlight = ($sunSouth, $sunWest, $sunEast);
my ($sunMin, $sunMax) = (sort {$a <=> $b} @sunlight)[0,-1];
$sunSouth = $sunSouth == 0 ? $brightness : $sunSouth;
$sunWest = $sunWest == 0 ? $brightness : $sunWest;
$sunEast = $sunEast == 0 ? $brightness : $sunEast;
$brightness = ($sunMax > 0) ? $sunMax : $brightness;
if (AttrVal($name, "brightnessDayNightCtrl", 'sensor') eq 'sensor') {
$twilightFlag = $twilightFlag eq '4A' ? 'night' : 'day';
} else {
$twilightFlag = ElsnerWS_swayCtrl($hash, "dayNight", $brightness, "brightnessDayNight", "brightnessDayNightDelay", 10, 20, 600, 600, 'night', 'day');
}
#$sunSouth = ElsnerWS_SMA($hash, 'sunSouth', $sunSouth, $averageOrder);
#$sunWest = ElsnerWS_SMA($hash, 'sunWest', $sunWest, $averageOrder);
#$sunEast = ElsnerWS_SMA($hash, 'sunEast', $sunEast, $averageOrder);
#$brightness = ElsnerWS_SMA($hash, 'brightness', $brightness, $averageOrder);
readingsBeginUpdate($hash);
$temperature = ElsnerWS_readingsBulkUpdate($hash, "temperature", ($temperatureSign eq '2B' ? '' : '-') . pack('H*', $temperature), 0.025, undef, "%0.1f");
$sunSouth = ElsnerWS_readingsBulkUpdate($hash, "sunSouth", $sunSouth, 0.1, 0.7, "%d");
$sunWest = ElsnerWS_readingsBulkUpdate($hash, "sunWest", $sunWest, 0.1, 0.7, "%d");
$sunEast = ElsnerWS_readingsBulkUpdate($hash, "sunEast", $sunEast, 0.1, 0.7, "%d");
$brightness = ElsnerWS_readingsBulkUpdate($hash, "brightness", $brightness, 0.1, 0.7, "%d");
$isRaining = $isRaining eq '4A' ? 'yes' : 'no';
$windSpeed = ElsnerWS_readingsBulkUpdate($hash, "windSpeed", pack('H*', $windSpeed), 0.1, 0.7, "%0.1f");
my @windStrength = (0.2, 1.5, 3.3, 5.4, 7.9, 10.7, 13.8, 17.1, 20.7, 24.4, 28.4, 32.6);
my $windStrength = 0;
while($windSpeed > $windStrength[$windStrength] && $windStrength <= @windStrength + 1) {
$windStrength ++;
}
if (exists $hash->{helper}{timer}{heartbeat}) {
readingsBulkUpdateIfChanged($hash, "dayNight", $twilightFlag);
readingsBulkUpdateIfChanged($hash, "isRaining", $isRaining);
readingsBulkUpdateIfChanged($hash, "windStrength", $windStrength);
readingsBulkUpdateIfChanged($hash, "isSunny", ElsnerWS_swayCtrl($hash, "isSunny", $brightness, "brightnessSunny", "brightnessSunnyDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnySouth", ElsnerWS_swayCtrl($hash, "isSunnySouth", $sunSouth, "brightnessSunnySouth", "brightnessSunnySouthDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnyWest", ElsnerWS_swayCtrl($hash, "isSunnyWest", $sunSouth, "brightnessSunnyWest", "brightnessSunnyWestDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnyEast", ElsnerWS_swayCtrl($hash, "isSunnyEast", $sunSouth, "brightnessSunnyEast", "brightnessSunnyEastDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isStormy", ElsnerWS_swayCtrl($hash, "isStormy", $windSpeed, "windSpeedStormy", "windSpeedStormyDelay", 13.9, 17.2, 60, 3, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isWindy", ElsnerWS_swayCtrl($hash, "isWindy", $windSpeed, "windSpeedWindy", "windSpeedWindyDelay", 1.6, 3.4, 60, 3, 'no', 'yes'));
} else {
readingsBulkUpdate($hash, "dayNight", $twilightFlag);
readingsBulkUpdate($hash, "isRaining", $isRaining);
readingsBulkUpdate($hash, "windStrength", $windStrength);
readingsBulkUpdate($hash, "isSunny", ElsnerWS_swayCtrl($hash, "isSunny", $brightness, "brightnessSunny", "brightnessSunnyDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnySouth", ElsnerWS_swayCtrl($hash, "isSunnySouth", $sunSouth, "brightnessSunnySouth", "brightnessSunnySouthDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnyWest", ElsnerWS_swayCtrl($hash, "isSunnyWest", $sunSouth, "brightnessSunnyWest", "brightnessSunnyWestDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnyEast", ElsnerWS_swayCtrl($hash, "isSunnyEast", $sunSouth, "brightnessSunnyEast", "brightnessSunnyEastDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isStormy", ElsnerWS_swayCtrl($hash, "isStormy", $windSpeed, "windSpeedStormy", "windSpeedStormyDelay", 13.9, 17.2, 60, 3, 'no', 'yes'));
readingsBulkUpdate($hash, "isWindy", ElsnerWS_swayCtrl($hash, "isWindy", $windSpeed, "windSpeedWindy", "windSpeedWindyDelay", 1.6, 3.4, 60, 3, 'no', 'yes'));
}
readingsBulkUpdateIfChanged($hash, "state", "T: " . $temperature .
" B: " . $brightness .
" W: " . $windSpeed .
" IR: " . $isRaining);
if (defined $zdata) {
if ($packetType eq '47') {
# packet type GPS
$hash->{MODEL} = 'GPS';
my %weekday = ('3F' => 'UTC_error',
'31' => 'Monday',
'32' => 'Tuesday',
'33' => 'Wednesday',
'34' => 'Thursday',
'35' => 'Friday',
'36' => 'Saturday',
'37' => 'Sunday');
$zdata =~ m/^(..)(....)(....)(....)(....)(....)(....)(..)(..........)(..)(........)(..)(..........)(..)(........)/;
my ($weekday, $day, $month, $year, $hour, $minute, $second, $gpsStatus, $sunAzimuth, $sunElevationSign, $sunElevation, $longitudeSign, $longitude, $latitudeSign, $latitude) =
($1, $2, $3, $4, $5, $6, $7, $8,$9, $10, $11, $12, $13, $14, $15);
if ($weekday eq '3F') {
# UTC error
readingsDelete($hash, 'weekday');
readingsDelete($hash, 'date');
readingsDelete($hash, 'timeZone');
readingsDelete($hash, 'time');
delete $hash->{GPS_TIME};
} else {
readingsBulkUpdateIfChanged($hash, "weekday", $weekday{$weekday});
readingsBulkUpdateIfChanged($hash, "date", '20' . pack('H*', $year) . '-' . pack('H*', $month) . '-' . pack('H*', $day));
readingsBulkUpdateIfChanged($hash, "timeZone", 'UTC');
#readingsBulkUpdateIfChanged($hash, "time", pack('H*', $hour) . ':' . pack('H*', $minute) . ':' . pack('H*', $second));
$hash->{GPS_TIME} = pack('H*', $hour) . ':' . pack('H*', $minute) . ':' . pack('H*', $second);
}
if ($gpsStatus eq '30') {
# GPS error
readingsDelete($hash, 'hemisphere');
readingsDelete($hash, 'latitude');
readingsDelete($hash, 'longitude');
readingsDelete($hash, 'sunAzimuth');
readingsDelete($hash, 'sunElevation');
readingsDelete($hash, 'twilight');
} else {
readingsBulkUpdateIfChanged($hash, "sunAzimuth", sprintf("%0.1f", pack('H*', $sunAzimuth)));
$sunElevation = ($sunElevationSign eq '2B' ? '' : '-') . pack('H*', $sunElevation);
my $twilight = ($sunElevation + 12) / 18 * 100;
$twilight = 0 if ($twilight < 0);
$twilight = 100 if ($twilight > 100);
readingsBulkUpdateIfChanged($hash, "sunElevation", sprintf("%0.1f", $sunElevation));
readingsBulkUpdateIfChanged($hash, "twilight", int($twilight));
readingsBulkUpdateIfChanged($hash, "longitude", sprintf("%0.1f", ($longitudeSign eq '4F' ? '' : '-') . pack('H*', $longitude)));
if (AttrVal($name, "updateGlobalAttr", 'no') eq 'yes') {
$attr{global}{longitude} = sprintf("%0.1f", ($longitudeSign eq '4F' ? '' : '-') . pack('H*', $longitude));
}
readingsBulkUpdateIfChanged($hash, "latitude", sprintf("%0.1f", ($latitudeSign eq '4E' ? '' : '-') . pack('H*', $latitude)));
if (AttrVal($name, "updateGlobalAttr", 'no') eq 'yes') {
$attr{global}{latitude} = sprintf("%0.1f", ($latitudeSign eq '4E' ? '' : '-') . pack('H*', $latitude));
}
readingsBulkUpdateIfChanged($hash, "hemisphere", ($latitudeSign eq '4E' ? "north" : "south"));
}
} elsif ($packetType eq '57') {
# packet type CET
$hash->{MODEL} = 'CET';
my %weekday = ('3F' => 'UTC_error',
'31' => 'Monday',
'32' => 'Tuesday',
'33' => 'Wednesday',
'34' => 'Thursday',
'35' => 'Friday',
'36' => 'Saturday',
'37' => 'Sunday');
$zdata =~ m/^(..)(....)(....)(....)(....)(....)(....)(..)/;
my ($weekday, $day, $month, $year, $hour, $minute, $second, $timeZone) = ($1, $2, $3, $4, $5, $6, $7, $8);
if ($weekday eq '3F') {
# UTC error
readingsDelete($hash, 'weekday');
readingsDelete($hash, 'date');
readingsDelete($hash, 'timeZone');
readingsDelete($hash, 'time');
delete $hash->{GPS_TIME};
} else {
readingsBulkUpdateIfChanged($hash, "weekday", $weekday{$weekday});
readingsBulkUpdateIfChanged($hash, "date", '20' . pack('H*', $year) . '-' . pack('H*', $month) . '-' . pack('H*', $day));
readingsBulkUpdateIfChanged($hash, "timeZone", $timeZone eq '4E' ? 'CET' : 'CEST');
$hash->{GPS_TIME} = pack('H*', $hour) . ':' . pack('H*', $minute) . ':' . pack('H*', $second);
}
}
} else {
$hash->{MODEL} = 'BASIC';
}
readingsEndUpdate($hash, 1);
readingsSingleUpdate($hash, 'time', $hash->{GPS_TIME}, AttrVal($name, 'timeEvent', 'no') eq 'yes' ? 1 : 0) if (exists $hash->{GPS_TIME});
$data = $rest;
readingsDelete($hash, 'alarm');
RemoveInternalTimer($hash->{helper}{timer}{alarm}) if(exists $hash->{helper}{timer}{alarm});
@{$hash->{helper}{timer}{alarm}} = ($hash, 'alarm', 'dead_sensor', 1, 5, 0);
InternalTimer(gettimeofday() + 1.5, 'ElsnerWS_readingsSingleUpdate', $hash->{helper}{timer}{alarm}, 0);
if (!exists $hash->{helper}{timer}{heartbeat}) {
@{$hash->{helper}{timer}{heartbeat}} = ($hash, 'heartbeat');
RemoveInternalTimer($hash->{helper}{timer}{heartbeat});
InternalTimer(gettimeofday() + 600, 'ElsnerWS_cdmClearTimer', $hash->{helper}{timer}{heartbeat}, 0);
#Log3 $hash->{NAME}, 3, "ElsnerWS $hash->{NAME} ElsnerWS_readingsBulkUpdate heartbeat executed.";
}
}
if(length($data) >= 4) {
$data =~ s/.*47/47/ if($data !~ m/^47(2B|2D)/);
$data =~ s/.*57/57/ if($data !~ m/^57(2B|2D)/);
$data = "" if($data !~ m/^47|57/);
}
$hash->{PARTIAL} = $data;
return;
}
sub ElsnerWS_SMA($$$$) {
# simple moving average (SMA)
my ($hash, $readingName, $readingVal, $averageOrder) = @_;
my $average = exists($hash->{helper}{sma}{$readingName}{average}) ? $hash->{helper}{sma}{$readingName}{average} : $readingVal;
my $numArrayElements = $#{$hash->{helper}{sma}{$readingName}{val}};
if (defined($numArrayElements) && $numArrayElements >= 0) {
if ($numArrayElements < $averageOrder - 1) {
$average = $average + $readingVal / ($numArrayElements + 1)
- $hash->{helper}{sma}{$readingName}{val}[$numArrayElements] / ($numArrayElements + 1);
} else {
$average = $average + $readingVal / ($numArrayElements + 1)
- pop(@{$hash->{helper}{sma}{$readingName}{val}}) / ($numArrayElements + 1);
}
} else {
$average = $readingVal;
}
unshift(@{$hash->{helper}{sma}{$readingName}{val}}, $readingVal);
$hash->{helper}{sma}{$readingName}{average} = $average;
return $average;
}
sub ElsnerWS_LWMA($$$$) {
# linear weighted moving average (LWMA)
my ($hash, $readingName, $readingVal, $averageOrder) = @_;
push(@{$hash->{helper}{lwma}{$readingName}{val}}, $readingVal);
my $average = 0;
my $numArrayElements = $#{$hash->{helper}{lwma}{$readingName}{val}} + 1;
for (my $i = 1; $i <= $numArrayElements; $i++) {
$average += $i * $hash->{helper}{lwma}{$readingName}{val}[$numArrayElements - $i];
}
$average = $average * 2 / $numArrayElements / ($numArrayElements + 1);
if ($numArrayElements >= $averageOrder) {
shift(@{$hash->{helper}{lwma}{$readingName}{val}});
}
return $average;
}
sub ElsnerWS_EMA($$$$) {
# exponential moving average (EMA)
# 0 < $wheight < 1
my ($hash, $readingName, $readingVal, $wheight) = @_;
my $average = exists($hash->{helper}{ema}{$readingName}{average}) ? $hash->{helper}{ema}{$readingName}{average} : $readingVal;
$average = $wheight * $readingVal + (1 - $wheight) * $average;
$hash->{helper}{ema}{$readingName}{average} = $average;
return $average;
}
sub ElsnerWS_Smooting($$$$$) {
my ($hash, $readingName, $readingVal, $theshold, $averageParam) = @_;
my $iniVal;
$readingVal = ElsnerWS_EMA($hash, $readingName, $readingVal, $averageParam) if (defined $averageParam);
if (exists $hash->{helper}{smooth}{$readingName}{iniVal}) {
$iniVal = $hash->{helper}{smooth}{$readingName}{iniVal};
if ($readingVal > $iniVal && $readingVal - $iniVal > $readingVal * $theshold ||
$readingVal < $iniVal && $iniVal - $readingVal > $iniVal * $theshold){
$iniVal = $readingVal;
$hash->{helper}{smooth}{$readingName}{iniVal} = $iniVal;
}
} else {
$iniVal = $readingVal;
$hash->{helper}{smooth}{$readingName}{iniVal} = $iniVal;
}
return $iniVal;
}
sub ElsnerWS_swayCtrl($$$$$$$$$$$) {
# sway range and delay calculation
my ($hash, $readingName, $readingVal, $attrNameRange, $attrNameDelay, $swayRangeLow, $swayRangeHigh, $swayDelayLow, $swayDelayHigh, $swayRangeLowVal, $swayRangeHighVal) = @_;
my ($swayRangeLowAttr, $swayRangeHighAttr) = split(':', AttrVal($hash->{NAME}, $attrNameRange, "$swayRangeLow:$swayRangeHigh"));
my ($swayDelayLowAttr, $swayDelayHighAttr) = split(':', AttrVal($hash->{NAME}, $attrNameDelay, "$swayDelayLow:$swayDelayHigh"));
my $swayValLast = exists($hash->{helper}{sway}{$readingName}) ? $hash->{helper}{sway}{$readingName} : $swayRangeLowVal;
my $swayVal = $swayValLast;
if (!defined($swayRangeLowAttr) && !defined($swayRangeHighAttr)) {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif (!defined($swayRangeLowAttr) && $swayRangeHighAttr eq '' || !defined($swayRangeHighAttr) && $swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif ($swayRangeHighAttr eq '' && $swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif ($swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeHighAttr;
} elsif ($swayRangeHighAttr eq '') {
$swayRangeHighAttr = $swayRangeLowAttr;
}
($swayRangeLowAttr, $swayRangeHighAttr) = ($swayRangeHighAttr, $swayRangeLowAttr) if ($swayRangeLowAttr > $swayRangeHighAttr);
if ($readingVal < $swayRangeLowAttr) {
$swayVal = $swayRangeLowVal;
} elsif ($readingVal >= $swayRangeHighAttr) {
$swayVal = $swayRangeHighVal;
} elsif ($readingVal >= $swayRangeLowAttr && $swayVal eq $swayRangeLowVal) {
$swayVal = $swayRangeLowVal;
} elsif ($readingVal < $swayRangeHighAttr && $swayVal eq $swayRangeHighVal) {
$swayVal = $swayRangeHighVal;
}
if (!defined($swayDelayLowAttr) && !defined($swayDelayHighAttr)) {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif (!defined($swayDelayLowAttr) && $swayDelayHighAttr eq '' || !defined($swayDelayHighAttr) && $swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif ($swayDelayHighAttr eq '' && $swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif ($swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayHighAttr;
} elsif ($swayDelayHighAttr eq '') {
$swayDelayHighAttr = $swayDelayLowAttr;
}
if ($swayVal eq $swayValLast) {
$hash->{helper}{sway}{$readingName} = $swayVal;
if (exists $hash->{helper}{timer}{sway}{$readingName}{delay}) {
# clear timer as sway reverses
RemoveInternalTimer($hash->{helper}{timer}{sway}{$readingName}{delay});
delete $hash->{helper}{timer}{sway}{$readingName}{delay};
}
} else {
$hash->{helper}{sway}{$readingName} = $swayValLast;
my $swayDelay = $swayVal eq $swayRangeHighVal ? $swayDelayHighAttr : $swayDelayLowAttr;
if (exists $hash->{helper}{timer}{sway}{$readingName}{delay}) {
$swayVal = $swayValLast;
} elsif ($swayDelay > 0) {
@{$hash->{helper}{timer}{sway}{$readingName}{delay}} = ($hash, $readingName, $swayVal, $swayDelay, 1, 5, 1);
InternalTimer(gettimeofday() + $swayDelay, 'ElsnerWS_swayCtrlDelay', $hash->{helper}{timer}{sway}{$readingName}{delay}, 0);
$swayVal = $swayValLast;
}
}
return $swayVal;
}
sub ElsnerWS_swayCtrlDelay($) {
my ($readingParam) = @_;
my ($hash, $readingName, $readingVal, $delay, $ctrl, $log, $clear) = @$readingParam;
if (defined $hash) {
readingsSingleUpdate($hash, $readingName, $readingVal, $ctrl);
Log3 $hash->{NAME}, $log, " ElsnerWS " . $hash->{NAME} . " EVENT $readingName: $readingVal" if ($log);
$hash->{helper}{sway}{$readingName} = $readingVal;
delete $hash->{helper}{timer}{sway}{$readingName}{delay} if ($clear == 1);
}
return;
}
sub ElsnerWS_readingsSingleUpdate($) {
my ($readingParam) = @_;
my ($hash, $readingName, $readingVal, $ctrl, $log, $clear) = @$readingParam;
if (defined $hash) {
readingsSingleUpdate($hash, $readingName, $readingVal, $ctrl) ;
Log3 $hash->{NAME}, $log, " ElsnerWS " . $hash->{NAME} . " EVENT $readingName: $readingVal" if ($log);
}
return;
}
sub ElsnerWS_readingsBulkUpdate($$$$$$) {
my ($hash, $readingName, $readingVal, $theshold, $averageParam, $sFormat) = @_;
if (exists $hash->{helper}{timer}{heartbeat}) {
$readingVal = sprintf("$sFormat", ElsnerWS_Smooting($hash, $readingName, $readingVal, $theshold, $averageParam));
readingsBulkUpdateIfChanged($hash, $readingName, $readingVal);
} else {
$readingVal = sprintf("$sFormat", ElsnerWS_Smooting($hash, $readingName, $readingVal, 0, $averageParam));
readingsBulkUpdate($hash, $readingName, $readingVal);
}
return $readingVal;
}
#
sub ElsnerWS_cdmClearTimer($) {
my ($functionArray) = @_;
my ($hash, $timer) = @$functionArray;
delete $hash->{helper}{timer}{$timer};
#Log3 $hash->{NAME}, 3, "ElsnerWS $hash->{NAME} ElsnerWS_cdmClearTimer $timer executed.";
return;
}
# Ready
sub ElsnerWS_Ready($) {
my ($hash) = @_;
return DevIo_OpenDev($hash, 1, undef) if($hash->{STATE} eq "disconnected");
# This is relevant for windows/USB only
my $po = $hash->{USBDev};
return undef if(!$po);
my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
return ($InBytes>0);
}
# Attributes check
sub ElsnerWS_Attr(@) {
my ($cmd, $name, $attrName, $attrVal) = @_;
my $hash = $defs{$name};
# return if attribute list is incomplete
return undef if (!$init_done);
my $err;
if ($attrName eq "brightnessDayNightCtrl") {
if (!defined $attrVal) {
} elsif ($attrVal !~ m/^custom|sensor$/) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrName =~ m/^brightness(DayNight|Sunny).*$/) {
if (!defined $attrVal) {
} else {
my ($attrVal0, $attrVal1) = split(':', $attrVal);
if (!defined($attrVal0) && !defined($attrVal1)) {
} else {
if (!defined $attrVal0 || $attrVal0 eq '') {
} elsif ($attrVal0 !~ m/^[+]?\d+$/ || $attrVal0 + 0 > 99000) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
if (!defined $attrVal1 || $attrVal1 eq '') {
} elsif ($attrVal1 !~ m/^[+]?\d+$/ || $attrVal1 + 0 > 99000) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
}
}
} elsif ($attrName eq "timeEvent") {
if (!defined $attrVal) {
} elsif ($attrVal !~ m/^yes|no$/) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrName eq "updateGlobalAttr") {
if (!defined $attrVal) {
} elsif ($attrVal !~ m/^yes|no$/) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrName =~ m/^windSpeed.*$/) {
my ($attrVal0, $attrVal1) = split(':', $attrVal);
if (!defined $attrVal1) {
if (!defined $attrVal0) {
} elsif ($attrVal0 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal0 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrVal1 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal1 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
} else {
if (!defined $attrVal0) {
} elsif ($attrVal0 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal0 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
}
}
return $err;
}
# Notify actions
sub ElsnerWS_Notify(@) {
my ($hash, $dev) = @_;
return "" if (IsDisabled($hash->{NAME}));
if ($dev->{NAME} eq "global" && grep (m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})) {
ElsnerWS_InitSerialCom($hash);
}
return undef;
}
# Undef
sub ElsnerWS_Undef($$) {
my ($hash, $arg) = @_;
DevIo_CloseDev($hash);
return undef;
}
# Delete
sub ElsnerWS_Delete($$) {
my ($hash, $name) = @_;
my $logName = "FileLog_$name";
my ($count, $gplotFile, $logFile, $weblinkName, $weblinkHash);
Log3 $name, 2, "ElsnerWS $name deleted";
# delete FileLog device and log files
if (exists $defs{$logName}) {
$logFile = $defs{$logName}{logfile};
$logFile =~ /^(.*)($name).*\.(.*)$/;
$logFile = $1 . $2 . "*." . $3;
CommandDelete(undef, "FileLog_$name");
Log3 $name, 2, "ElsnerWS FileLog_$name deleted";
$count = unlink glob $logFile;
Log3 $name, 2, "ElsnerWS $logFile >> $count files deleted";
}
# delete SVG devices and gplot files
while (($weblinkName, $weblinkHash) = each(%defs)) {
if ($weblinkName =~ /^SVG_$name.*/) {
$gplotFile = "./www/gplot/" . $defs{$weblinkName}{GPLOTFILE} . "*.gplot";
CommandDelete(undef, $weblinkName);
Log3 $name, 2, "ElsnerWS $weblinkName deleted";
$count = unlink glob $gplotFile;
Log3 $name, 2, "ElsnerWS $gplotFile >> $count files deleted";
}
}
return undef;
}
1;
=pod
=item summary ElsnerWS Elsner Weather Station P03/3-RS485 or P04/3-RS485 evaluation modul
=item summary_DE ElsnerWS Elsner Wetterstation P03/3-RS485 oder P04/3-RS485 Auswertemodul
=begin html
<a name="ElsnerWS"></a>
<h3>ElsnerWS</h3>
<ul>
The ElsnerWS weather evaluation modul serves as a connecting link between the
<a href="https://www.elsner-elektronik.de/shop/de/produkte-shop/gebaeudetechnik-konventionell/rs485-sensoren/p03-3-rs485.html">Elsner P03/3-RS485 Weather Stations</a> or
<a href="https://www.elsner-elektronik.de/shop/de/produkte-shop/gebaeudetechnik-konventionell/rs485-sensoren/p04-3-rs485.html">Elsner P04/3-RS485 Weather Stations</a> or
and blind actuators. ElsnerWS use a RS485 connection to communicate with the
<a href="https://www.elsner-elektronik.de/shop/de/produkte-shop/gebaeudetechnik-konventionell/rs485-sensoren.html">Elsner P0x/3-RS485 Weather Stations</a>.
It received the raw weather data periodically once a second, which manufacturer-specific coded via an RS485 bus serially are sent.
It evaluates the received weather data and based on adjustable thresholds and delay times it generates up/down signals
for blinds according to wind, rain and sun. This way blinds can be pilot-controlled and protected against thunderstorms.
The GPS function of the sensor provides the current values of the date, time, weekday, sun azimuth, sun elevation, longitude
and latitude.<br>
As an alternative to the module for the Elsner RS485 sensors, sensors with Modbus RS485 protocol can also be used.
The <a href="#ModbusElsnerWS">ModbusElsnerWS</a> module is available for this purpose. EnOcean profiles
"Environmental Applications" with the EEP A5-13-01 ... EEP A5-13-06 are also available for these weather stations,
but they also require weather evaluation units from Eltako or AWAG, for example. The functional scope of the
modules is widely similar.
<br><br>
<b>Functions</b>
<ul>
<li>Evaluation modul for the weather sensors P03/3-RS485 or P04/3-RS485 (Basic|CET|GPS)</li>
<li>Processing weather raw data and creates graphic representation</li>
<li>Alarm signal in case of failure of the weather sensor</li>
<li>Up/down readings for blinds according to wind, rain and sun</li>
<li>Adjustable switching thresholds and delay times</li>
<li>Day/night signal</li>
<li>Display of date, time, sun azimuth, sun elevation, longitude and latitude</li>
</ul><br>
<b>Prerequisites</b>
<ul>
This module requires the basic Device::SerialPort or Win32::SerialPort module.
</ul><br>
<b>Hardware Connection</b>
<ul>
The weather sensors P03/3-RS485 or P04/3-RS485 are connected via a shielded cable 2x2x0.5 mm2 to a RS485 transceiver.
The sensor is connected via the pins A to the RS485 B(+)-Port, B to RS485 A(-)-Port, 1 to + 24 V, 2 to GND and Shield.
Please note that the RS485 connection names are reversed. Only the usual pins for serial Modbus communication A, B and Ground are needed. Multiple Fhem devices can be connected to the sensor
via the RS485 bus at the same time.<br>
The serial bus should be terminated at its most remote ends with 120 Ohm resistors. If several RS485 transceiver are connected to
the serial bus, only the termination resistor in the devices furthest ends must be switched on.<br>
More information about the sensors, see for example
<a href="https://www.elsner-elektronik.de/shop/de/fileuploader/download/download/?d=1&file=custom%2Fupload%2F30145_P033-RS485-GPS_Datenblatt_13Sep18_DBEEA6042.pdf">P03/3-RS485-GPS User Guide</a>.<br>
The USB adapters
<a href="https://www.digitus.info/produkte/computer-zubehoer-und-komponenten/computer-zubehoer/seriell-und-parallel-adapter/da-70157/">Digitus DA-70157</a>,
<a href="https://shop.in-circuit.de/product_info.php?cPath=33&products_id=81">In-Circuit USB-RS485-Bridge</a>
and <a href="http://www.dsdtech-global.com/2018/01/sh-u10-spp.html">DSD TECH SH-U10 USB to RS485 converter</a>
are successfully tested at a Raspberry PI in conjunction with the weather sensor.
</ul><br>
<a name="ElsnerWSDefine"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; ElsnerWS comtype=&lt;comtype&gt; devicename=&lt;devicename&gt;</code><br><br>
The module connects to the Elsner Weather Station via serial bus &lt;rs485&gt; through the device &lt;device&gt;.<br>
The following parameters apply to an RS485 transceiver to USB.
<br><br>
Example:<br>
<code>define WS ElsnerWS comtype=rs485 devicename=/dev/ttyUSB1@19200</code><br>
<code>define WS ElsnerWS comtype=rs485 devicename=COM1@19200</code> (Windows)
<br><br>
Alternatively, the device can also be created automatically by autocreate. Once the weather station is connected
to Fhem via the RS485 USB transceiver, Fhem is to be restarted. The active but not yet configured USB ports are
searched for a ready-to-operate weather station during the Fhem boot.
</ul><br>
<a name="ElsnerWSattr"></a>
<b>Attributes</b>
<ul>
<ul>
<li><a name="ElsnerWS_brightnessDayNight">brightnessDayNight</a> E_min/lx:E_max/lx,
[brightnessDayNight] = 0...99000:0...99000, 10:20 is default.<br>
Set switching thresholds for reading dayNight based on the reading brightness.
</li>
<li><a name="ElsnerWS_brightnessDayNightCtrl">brightnessDayNightCtrl</a> custom|sensor,
[brightnessDayNightCtrl] = custom|sensor, sensor is default.<br>
Control the dayNight reading through the device-specific or custom threshold and delay.
</li>
<li><a name="ElsnerWS_brightnessDayNightDelay">brightnessDayNightDelay</a> t_reset/s:t_set/s,
[brightnessDayNightDelay] = 0...99000:0...99000, 600:600 is default.<br>
Set switching delay for reading dayNight based on the reading brightness. The reading dayNight is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_brightnessSunny">brightnessSunny</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunny based on the reading brightness.
</li>
<li><a name="ElsnerWS_brightnessSunnyDelay">brightnessSunnyDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunny based on the reading brightness. The reading isSunny is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_brightnessSunnyEast">brightnessSunnyEast</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnyEast based on the reading sunEast.
</li>
<li><a name="ElsnerWS_brightnessSunnyEastDelay">brightnessSunnyEastDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnyEast based on the reading sunEast. The reading isSunnyEast is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_brightnessSunnySouth">brightnessSunnySouth</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnySouth based on the reading sunSouth.
</li>
<li><a name="ElsnerWS_brightnessSunnySouthDelay">brightnessSunnySouthDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnySouth based on the reading sunSouth. The reading isSunnySouth is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_brightnessSunnyWest">brightnessSunnyWest</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnyWest based on the reading sunWest.
</li>
<li><a name="ElsnerWS_brightnessSunnyWestDelay">brightnessSunnyWestDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnyWest based on the reading sunWest. The reading isSunnyWest is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
<li><a name="ElsnerWS_timeEvent">timeEvent</a> no|yes,
[timeEvent] = no|yes, no is default.<br>
Update the reading time periodically.
</li>
<li><a name="ElsnerWS_windSpeedStormy">windSpeedStormy</a> v_min/m/s:v_max/m/s,
[windSpeedStormy] = 0...35:0...35, 13.9:17.2 is default.<br>
Set switching thresholds for reading isStormy based on the reading windSpeed.
</li>
<li><a name="ElsnerWS_windSpeedStormyDelay">windSpeedStormyDelay</a> t_reset/s:t_set/s,
[windSpeedStormyDelay] = 0...99000:0...99000, 60:3 is default.<br>
Set switching delay for reading isStormy based on the reading windSpeed. The reading isStormy is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_windSpeedWindy">windSpeedWindy</a> v_min/m/s:v_max/m/s,
[windSpeedWindy] = 0...35:0...35, 1.6:3.4 is default.<br>
Set switching thresholds for reading isWindy based on the reading windSpeed.
</li>
<li><a name="ElsnerWS_windSpeedWindyDelay">windSpeedWindyDelay</a> t_reset/s:t_set/s,
[windSpeedWindyDelay] = 0...99000:0...99000, 60:3 is default.<br>
Set switching delay for reading isWindy based on the reading windSpeed. The reading isWindy is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ElsnerWS_updateGlobalAttr">updateGlobalAttr</a> no|yes,
[timeEvent] = no|yes, no is default.<br>
Update the global attributes latitude and longitude with the received GPS coordinates.
</li>
</ul>
</ul><br>
<a name="ElsnerWSevents"></a>
<b>Generated events</b>
<ul>
<ul>
<li>T: t/&#176C B: E/lx W: v/m/s IR: no|yes</li>
<li>brightness: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>date: JJJJ-MM-TT</li>
<li>dayNight: day|night</li>
<li>hemisphere: north|south</li>
<li>isRaining: no|yes</li>
<li>isStormy: no|yes</li>
<li>isSunny: no|yes</li>
<li>isSunnyEast: no|yes</li>
<li>isSunnySouth: no|yes</li>
<li>isSunnyWest: no|yes</li>
<li>isWindy: no|yes</li>
<li>latitude: &phi;/&deg; (Sensor Range: &phi; = -90 &deg; ... 90 &deg;)</li>
<li>longitude: &lambda;/&deg; (Sensor Range: &lambda; = -180 &deg; ... 180 &deg;)</li>
<li>sunAzimuth: &alpha;/&deg; (Sensor Range: &alpha; = 0 &deg; ... 359 &deg;)</li>
<li>sunEast: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>sunElevation: &beta;/&deg; (Sensor Range: &beta; = -90 &deg; ... 90 &deg;)</li>
<li>sunSouth: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>sunWest: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>temperature: t/&#176C (Sensor Range: t = -40 &#176C ... 70 &#176C)</li>
<li>time: hh:mm:ss</li>
<li>timeZone: CET|CEST|UTC</li>
<li>weekday: Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday</li>
<li>twilight: T/% (Sensor Range: T = 0 % ... 100 %)</li>
<li>windSpeed: v/m/s (Sensor Range: v = 0 m/s ... 70 m/s)</li>
<li>windStrength: B (Sensor Range: B = 0 Beaufort ... 12 Beaufort)</li>
<li>state: T: t/&#176C B: E/lx W: v/m/s IR: no|yes</li>
</ul>
</ul>
</ul>
=end html
=cut

View File

@ -0,0 +1,767 @@
# $Id$
# This modules handles the communication with a Elsner Weather Station P03/3-Modbus.
package main;
use strict;
use warnings;
sub ModbusElsnerWS_Initialize($);
my %ModbusElsnerWS_ParseInfo = (
'i0' => {reading => 'raw',
name => 'raw',
expr => 'sprintf("%.1f %d %d %d %d %.1f %d %s %04d-%02d-%02d %02d:%02d:%02d %.1f %.1f %.2f %.2f",
$val[0] / 10,
$val[1] * 1000,
$val[2] * 1000,
$val[3] * 1000,
$val[4],
$val[5] / 10,
$val[6],
$val[7] == 1 ? "yes" : "no",
$val[10],
$val[9],
$val[8],
$val[11],
$val[12],
$val[13],
$val[14] / 10,
$val[15] / 10,
$val[16] / 100,
$val[17] / 100)',
unpack => 's>nnnnnCCnnnnnnns>s>s>',
len => 17,
poll => 1
}
);
my %ModbusElsnerWS_DeviceInfo = (
'i' => {defShowGet => 1,
combine => 17
}
);
sub ModbusElsnerWS_Initialize($) {
my ($hash) = @_;
require "$attr{global}{modpath}/FHEM/DevIo.pm";
LoadModule "Modbus";
$hash->{parseInfo} = \%ModbusElsnerWS_ParseInfo; # defines registers, inputs, coils etc. for this Modbus Device
$hash->{deviceInfo} = \%ModbusElsnerWS_DeviceInfo; # defines properties of the device like defaults and supported function codes
ModbusLD_Initialize($hash); # Generic function of the Modbus module does the rest
$hash->{DefFn} = "ModbusElsnerWS_Define";
$hash->{UndefFn} = "ModbusElsnerWS_Undef";
$hash->{DeleteFn} = "ModbusElsnerWS_Delete";
$hash->{SetFn} = "ModbusElsnerWS_Set";
$hash->{GetFn} = "ModbusElsnerWS_Get";
$hash->{NotifyFn} = "ModbusElsnerWS_Notify";
$hash->{ModbusReadingsFn} = "ModbusElsnerWS_Eval";
$hash->{AttrFn} = "ModbusElsnerWS_Attr";
$hash->{AttrList} .= ' ' .
#$hash->{ObjAttrList} . ' ' . $hash->{DevAttrList}.
' brightnessDayNight brightnessDayNightDelay' .
' brightnessSunny brightnessSunnySouth brightnessSunnyWest brightnessSunnyEast' .
' brightnessSunnyDelay brightnessSunnySouthDelay brightnessSunnyWestDelay brightnessSunnyEastDelay' .
' poll-.* polldelay-.* timeEvent:no,yes updateGlobalAttr:no,yes' .
' windSpeedWindy windSpeedStormy windSpeedWindyDelay windSpeedStormyDelay ' .
$readingFnAttributes;
$hash->{parseParams} = 1;
$hash->{NotifyOrderPrefix} = "55-";
return;
}
sub ModbusElsnerWS_Define($$) {
# my ($hash, $def) = @_;
#return "ModbusElsnerWS: wrong syntax, correct is: define <name> ModbusElsnerWS (modbusid) {interval}" if($#$a != 2);
my ($hash, $a, $h) = @_;
my $name = $a->[0];
if (defined $a->[2]) {
$hash->{MODBUSID} = $a->[2];
} elsif (exists $h->{id}) {
$hash->{MODBUSID} = $h->{id};
} else {
return "ModbusElsnerWS: wrong syntax, correct is: define <name> ModbusElsnerWS [id=](modbusid) [interval=]{interval}";
}
if (defined $a->[3]) {
$hash->{INTERVAL} = $a->[3];
} elsif (exists $h->{interval}) {
$hash->{INTERVAL} = $h->{interval};
} else {
return "ModbusElsnerWS: wrong syntax, correct is: define <name> ModbusElsnerWS [id=](modbusid) [interval=]{interval}";
}
if ($hash->{INTERVAL} !~ m/^(\d+|passive)$/) {
return "ModbusElsnerWS: wrong syntax, correct is: interval >= 1 or passive";
}
my $def = "$name ModbusElsnerWS $hash->{MODBUSID} $hash->{INTERVAL}";
$hash->{NOTIFYDEV} = "global";
# deactivate modbus set commands
$attr{$name}{enableControlSet} = 0;
my ($autocreateFilelog, $autocreateHash, $autocreateName, $autocreateDeviceRoom, $autocreateWeblinkRoom) =
('./log/' . $name . '-%Y-%m.log', undef, 'autocreate', 'ElsnerWS', 'Plots');
my ($cmd, $filelogName, $gplot, $ret, $weblinkName, $weblinkHash) =
(undef, "FileLog_$name", "ElsnerWS:SunIntensity,ElsnerWS_2:Temperature/Brightness,ElsnerWS_3:WindSpeed/Raining,", undef, undef, undef);
# find autocreate device
while (($autocreateName, $autocreateHash) = each(%defs)) {
last if ($defs{$autocreateName}{TYPE} eq "autocreate");
}
$autocreateDeviceRoom = AttrVal($autocreateName, "device_room", $autocreateDeviceRoom) if (defined $autocreateName);
$autocreateDeviceRoom = 'ElsnerWS' if ($autocreateDeviceRoom eq '%TYPE');
$autocreateDeviceRoom = $name if ($autocreateDeviceRoom eq '%NAME');
$autocreateDeviceRoom = AttrVal($name, "room", $autocreateDeviceRoom);
$attr{$name}{room} = $autocreateDeviceRoom if (!exists $attr{$name}{room});
if ($init_done) {
Log3 $name, 2, "ModbusElsnerWS define " . join(' ', @$a);
if (!defined(AttrVal($autocreateName, "disable", undef)) && !exists($defs{$filelogName})) {
# create FileLog
$autocreateFilelog = $attr{$autocreateName}{filelog} if (exists $attr{$autocreateName}{filelog});
$autocreateFilelog =~ s/%NAME/$name/g;
$cmd = "$filelogName FileLog $autocreateFilelog $name";
$ret = CommandDefine(undef, $cmd);
if($ret) {
Log3 $filelogName, 2, "ModbusElsnerWS ERROR: $ret";
} else {
$attr{$filelogName}{room} = $autocreateDeviceRoom;
$attr{$filelogName}{logtype} = 'text';
Log3 $filelogName, 2, "ModbusElsnerWS define $cmd";
}
}
if (!defined(AttrVal($autocreateName, "disable", undef)) && exists($defs{$filelogName})) {
# create FileLog
# add GPLOT parameters
$attr{$filelogName}{logtype} = $gplot . $attr{$filelogName}{logtype}
if (!exists($attr{$filelogName}{logtype}) || $attr{$filelogName}{logtype} eq 'text');
if (AttrVal($autocreateName, "weblink", 1)) {
$autocreateWeblinkRoom = $attr{$autocreateName}{weblink_room} if (exists $attr{$autocreateName}{weblink_room});
$autocreateWeblinkRoom = 'ElsnerWS' if ($autocreateWeblinkRoom eq '%TYPE');
$autocreateWeblinkRoom = $name if ($autocreateWeblinkRoom eq '%NAME');
$autocreateWeblinkRoom = $attr{$name}{room} if (exists $attr{$name}{room});
my $wnr = 1;
#create SVG devices
foreach my $wdef (split(/,/, $gplot)) {
next if(!$wdef);
my ($gplotfile, $stuff) = split(/:/, $wdef);
next if(!$gplotfile);
$weblinkName = "SVG_$name";
$weblinkName .= "_$wnr" if($wnr > 1);
$wnr++;
next if (exists $defs{$weblinkName});
$cmd = "$weblinkName SVG $filelogName:$gplotfile:CURRENT";
Log3 $weblinkName, 2, "ModbusElsnerWS define $cmd";
$ret = CommandDefine(undef, $cmd);
if($ret) {
Log3 $weblinkName, 2, "ModbusElsnerWS ERROR: define $cmd: $ret";
last;
}
$attr{$weblinkName}{room} = $autocreateWeblinkRoom;
$attr{$weblinkName}{title} = '"' . $name . ' Min $data{min1}, Max $data{max1}, Last $data{currval1}"';
$ret = CommandSet(undef, "$weblinkName copyGplotFile");
if($ret) {
Log3 $weblinkName, 2, "ModbusElsnerWS ERROR: set $weblinkName copyGplotFile: $ret";
last;
}
}
}
}
}
ModbusLD_Define($hash, $def);
return;
}
sub ModbusElsnerWS_Eval($$$) {
my ($hash, $readingName, $readingVal) = @_;
my $name = $hash->{NAME};
my $ctrl = 1;
my ($temperature, $sunSouth, $sunWest, $sunEast, $brightness, $windSpeed, $gps, $isRaining, $date, $time, $sunAzimuth, $sunElevation, $latitude, $longitude) = split(' ', $readingVal);
my @sunlight = ($sunSouth, $sunWest, $sunEast);
my ($sunMin, $sunMax) = (sort {$a <=> $b} @sunlight)[0,-1];
$sunSouth = $sunSouth == 0 ? $brightness : $sunSouth;
$sunWest = $sunWest == 0 ? $brightness : $sunWest;
$sunEast = $sunEast == 0 ? $brightness : $sunEast;
$brightness = ($sunMax > 0) ? $sunMax : $brightness;
readingsBeginUpdate($hash);
$temperature = ModbusElsnerWS_readingsBulkUpdate($hash, "temperature", $temperature, 0.025, undef, "%0.1f");
$sunSouth = ModbusElsnerWS_readingsBulkUpdate($hash, "sunSouth", $sunSouth, 0.1, 0.7, "%d");
$sunWest = ModbusElsnerWS_readingsBulkUpdate($hash, "sunWest", $sunWest, 0.1, 0.7, "%d");
$sunEast = ModbusElsnerWS_readingsBulkUpdate($hash, "sunEast", $sunEast, 0.1, 0.7, "%d");
$brightness = ModbusElsnerWS_readingsBulkUpdate($hash, "brightness", $brightness, 0.1, 0.7, "%d");
$windSpeed = ModbusElsnerWS_readingsBulkUpdate($hash, "windSpeed", $windSpeed, 0.1, 0.3, "%0.1f");
my @windStrength = (0.2, 1.5, 3.3, 5.4, 7.9, 10.7, 13.8, 17.1, 20.7, 24.4, 28.4, 32.6);
my $windStrength = 0;
while($windSpeed > $windStrength[$windStrength] && $windStrength <= @windStrength + 1) {
$windStrength ++;
}
if (exists $hash->{helper}{timer}{heartbeat}) {
readingsBulkUpdateIfChanged($hash, "isRaining", $isRaining);
readingsBulkUpdateIfChanged($hash, "windStrength", $windStrength);
readingsBulkUpdateIfChanged($hash, "dayNight", ModbusElsnerWS_swayCtrl($hash, "dayNight", $brightness, "brightnessDayNight", "brightnessDayNightDelay", 10, 20, 600, 600, 'night', 'day'));
readingsBulkUpdateIfChanged($hash, "isSunny", ModbusElsnerWS_swayCtrl($hash, "isSunny", $brightness, "brightnessSunny", "brightnessSunnyDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnySouth", ModbusElsnerWS_swayCtrl($hash, "isSunnySouth", $sunSouth, "brightnessSunnySouth", "brightnessSunnySouthDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnyWest", ModbusElsnerWS_swayCtrl($hash, "isSunnyWest", $sunSouth, "brightnessSunnyWest", "brightnessSunnyWestDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isSunnyEast", ModbusElsnerWS_swayCtrl($hash, "isSunnyEast", $sunSouth, "brightnessSunnyEast", "brightnessSunnyEastDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isStormy", ModbusElsnerWS_swayCtrl($hash, "isStormy", $windSpeed, "windSpeedStormy", "windSpeedStormyDelay", 13.9, 17.2, 60, 3, 'no', 'yes'));
readingsBulkUpdateIfChanged($hash, "isWindy", ModbusElsnerWS_swayCtrl($hash, "isWindy", $windSpeed, "windSpeedWindy", "windSpeedWindyDelay", 1.6, 3.4, 60, 3, 'no', 'yes'));
} else {
readingsBulkUpdate($hash, "isRaining", $isRaining);
readingsBulkUpdate($hash, "windStrength", $windStrength);
readingsBulkUpdate($hash, "dayNight", ModbusElsnerWS_swayCtrl($hash, "dayNight", $brightness, "dayNightSwitch", "dayNightSwitchDelay", 10, 20, 600, 600, 'night', 'day'));
readingsBulkUpdate($hash, "isSunny", ModbusElsnerWS_swayCtrl($hash, "isSunny", $brightness, "brightnessSunny", "brightnessSunnyDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnySouth", ModbusElsnerWS_swayCtrl($hash, "isSunnySouth", $sunSouth, "brightnessSunnySouth", "brightnessSunnySouthDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnyWest", ModbusElsnerWS_swayCtrl($hash, "isSunnyWest", $sunSouth, "brightnessSunnyWest", "brightnessSunnyWestDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isSunnyEast", ModbusElsnerWS_swayCtrl($hash, "isSunnyEast", $sunSouth, "brightnessSunnyEast", "brightnessSunnyEastDelay", 20000, 40000, 120, 30, 'no', 'yes'));
readingsBulkUpdate($hash, "isStormy", ModbusElsnerWS_swayCtrl($hash, "isStormy", $windSpeed, "windSpeedStormy", "windSpeedStormyDelay", 13.9, 17.2, 60, 3, 'no', 'yes'));
readingsBulkUpdate($hash, "isWindy", ModbusElsnerWS_swayCtrl($hash, "isWindy", $windSpeed, "windSpeedWindy", "windSpeedWindyDelay", 1.6, 3.4, 60, 3, 'no', 'yes'));
}
readingsBulkUpdateIfChanged($hash, "state", "T: " . $temperature .
" B: " . $brightness .
" W: " . $windSpeed .
" IR: " . $isRaining);
if ($gps == 1) {
readingsBulkUpdateIfChanged($hash, "date", $date);
readingsBulkUpdateIfChanged($hash, "timeZone", 'UTC');
$hash->{GPS_TIME} = $time;
readingsBulkUpdateIfChanged($hash, "sunAzimuth", $sunAzimuth);
my $twilight = ($sunElevation + 12) / 18 * 100;
$twilight = 0 if ($twilight < 0);
$twilight = 100 if ($twilight > 100);
readingsBulkUpdateIfChanged($hash, "sunElevation", $sunElevation);
readingsBulkUpdateIfChanged($hash, "twilight", int($twilight));
readingsBulkUpdateIfChanged($hash, "longitude", $longitude);
if (AttrVal($name, "updateGlobalAttr", 'no') eq 'yes') {
$attr{global}{longitude} = $longitude;
}
readingsBulkUpdateIfChanged($hash, "latitude", $latitude);
if (AttrVal($name, "updateGlobalAttr", 'no') eq 'yes') {
$attr{global}{latitude} = $latitude;
}
readingsBulkUpdateIfChanged($hash, "hemisphere", $latitude < 0 ? "south" : "north");
} else {
delete $hash->{GPS_TIME};
readingsDelete($hash, 'date');
readingsDelete($hash, 'hemisphere');
readingsDelete($hash, 'latitude');
readingsDelete($hash, 'longitude');
readingsDelete($hash, 'sunAzimuth');
readingsDelete($hash, 'sunElevation');
readingsDelete($hash, 'time');
readingsDelete($hash, 'timeZone');
readingsDelete($hash, 'twilight');
}
readingsEndUpdate($hash, 1);
readingsSingleUpdate($hash, 'time', $hash->{GPS_TIME}, AttrVal($name, 'timeEvent', 'no') eq 'yes' ? 1 : 0) if (exists $hash->{GPS_TIME});
readingsDelete($hash, 'alarm');
RemoveInternalTimer($hash->{helper}{timer}{alarm}) if(exists $hash->{helper}{timer}{alarm});
@{$hash->{helper}{timer}{alarm}} = ($hash, 'alarm', 'dead_sensor', 1, 5, 0);
InternalTimer(gettimeofday() + $hash->{INTERVAL} * 1.5, 'ModbusElsnerWS_readingsSingleUpdate', $hash->{helper}{timer}{alarm}, 0);
if (!exists $hash->{helper}{timer}{heartbeat}) {
@{$hash->{helper}{timer}{heartbeat}} = ($hash, 'heartbeat');
RemoveInternalTimer($hash->{helper}{timer}{heartbeat});
InternalTimer(gettimeofday() + 600, 'ModbusElsnerWS_cdmClearTimer', $hash->{helper}{timer}{heartbeat}, 0);
#Log3 $hash->{NAME}, 3, "ModbusElsnerWS $hash->{NAME} ModbusElsnerWS_readingsBulkUpdate heartbeat executed.";
}
#Log3 $hash->{NAME}, 2, "ModbusElsnerWS_Eval $hash->{NAME} $readingName = $readingVal received";
return $ctrl;
}
sub ModbusElsnerWS_Attr(@) {
my ($cmd, $name, $attrName, $attrVal) = @_;
my $hash = $defs{$name};
# return if attribute list is incomplete
return undef if (!$init_done);
my $err;
if ($attrName =~ m/^brightness(DayNight|Sunny).*$/) {
if (!defined $attrVal) {
} else {
my ($attrVal0, $attrVal1) = split(':', $attrVal);
if (!defined($attrVal0) && !defined($attrVal1)) {
} else {
if (!defined $attrVal0 || $attrVal0 eq '') {
} elsif ($attrVal0 !~ m/^[+]?\d+$/ || $attrVal0 + 0 > 99000) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
if (!defined $attrVal1 || $attrVal1 eq '') {
} elsif ($attrVal1 !~ m/^[+]?\d+$/ || $attrVal1 + 0 > 99000) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
}
}
} elsif ($attrName eq "timeEvent") {
if (!defined $attrVal) {
} elsif ($attrVal !~ m/^yes|no$/) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrName eq "updateGlobalAttr") {
if (!defined $attrVal) {
} elsif ($attrVal !~ m/^yes|no$/) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrName =~ m/^windSpeed.*$/) {
my ($attrVal0, $attrVal1) = split(':', $attrVal);
if (!defined $attrVal1) {
if (!defined $attrVal0) {
} elsif ($attrVal0 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal0 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
} elsif ($attrVal1 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal1 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
} else {
if (!defined $attrVal0) {
} elsif ($attrVal0 !~ m/^[+]?\d+(\.\d)?$/ || $attrVal0 + 0 > 35) {
$err = "attribute-value [$attrName] = $attrVal wrong";
CommandDeleteAttr(undef, "$name $attrName");
}
}
}
ModbusLD_Attr($cmd, $name, $attrName, $attrVal);
return $err;
}
sub ModbusElsnerWS_Get($$$) {
my ($hash, $a, $h) = @_;
my ($result, $name, $opt) = (undef, $a->[1], $a->[2]);
$result = ModbusLD_Get($hash, @$a);
return $result;
}
sub ModbusElsnerWS_Set($$$) {
my ($hash, $a, $h) = @_;
my ($error, $name, $cmd) = (undef, $a->[1], $a->[2]);
$error = ModbusLD_Set($hash, @$a);
return $error;
}
sub ModbusElsnerWS_Notify(@) {
my ($hash, $dev) = @_;
return "" if (IsDisabled($hash->{NAME}));
return undef if (!$init_done);
if ($dev->{NAME} eq "global" && grep (m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})) {
}
Modbus_Notify($hash, $dev);
return undef;
}
sub ModbusElsnerWS_SMA($$$$) {
# simple moving average (SMA)
my ($hash, $readingName, $readingVal, $averageOrder) = @_;
my $average = exists($hash->{helper}{sma}{$readingName}{average}) ? $hash->{helper}{sma}{$readingName}{average} : $readingVal;
my $numArrayElements = $#{$hash->{helper}{sma}{$readingName}{val}};
if (defined($numArrayElements) && $numArrayElements >= 0) {
if ($numArrayElements < $averageOrder - 1) {
$average = $average + $readingVal / ($numArrayElements + 1)
- $hash->{helper}{sma}{$readingName}{val}[$numArrayElements] / ($numArrayElements + 1);
} else {
$average = $average + $readingVal / ($numArrayElements + 1)
- pop(@{$hash->{helper}{sma}{$readingName}{val}}) / ($numArrayElements + 1);
}
} else {
$average = $readingVal;
}
unshift(@{$hash->{helper}{sma}{$readingName}{val}}, $readingVal);
$hash->{helper}{sma}{$readingName}{average} = $average;
return $average;
}
sub ModbusElsnerWS_LWMA($$$$) {
# linear weighted moving average (LWMA)
my ($hash, $readingName, $readingVal, $averageOrder) = @_;
push(@{$hash->{helper}{lwma}{$readingName}{val}}, $readingVal);
my $average = 0;
my $numArrayElements = $#{$hash->{helper}{lwma}{$readingName}{val}} + 1;
for (my $i = 1; $i <= $numArrayElements; $i++) {
$average += $i * $hash->{helper}{lwma}{$readingName}{val}[$numArrayElements - $i];
}
$average = $average * 2 / $numArrayElements / ($numArrayElements + 1);
if ($numArrayElements >= $averageOrder) {
shift(@{$hash->{helper}{lwma}{$readingName}{val}});
}
return $average;
}
sub ModbusElsnerWS_EMA($$$$) {
# exponential moving average (EMA)
# 0 < $wheight < 1
my ($hash, $readingName, $readingVal, $wheight) = @_;
my $average = exists($hash->{helper}{ema}{$readingName}{average}) ? $hash->{helper}{ema}{$readingName}{average} : $readingVal;
$average = $wheight * $readingVal + (1 - $wheight) * $average;
$hash->{helper}{ema}{$readingName}{average} = $average;
return $average;
}
sub ModbusElsnerWS_Smooting($$$$$) {
my ($hash, $readingName, $readingVal, $theshold, $averageParam) = @_;
my $iniVal;
$readingVal = ModbusElsnerWS_EMA($hash, $readingName, $readingVal, $averageParam) if (defined $averageParam);
if (exists $hash->{helper}{smooth}{$readingName}{iniVal}) {
$iniVal = $hash->{helper}{smooth}{$readingName}{iniVal};
if ($readingVal > $iniVal && $readingVal - $iniVal > $readingVal * $theshold ||
$readingVal < $iniVal && $iniVal - $readingVal > $iniVal * $theshold){
$iniVal = $readingVal;
$hash->{helper}{smooth}{$readingName}{iniVal} = $iniVal;
}
} else {
$iniVal = $readingVal;
$hash->{helper}{smooth}{$readingName}{iniVal} = $iniVal;
}
return $iniVal;
}
sub ModbusElsnerWS_swayCtrl($$$$$$$$$$$) {
# sway range and delay calculation
my ($hash, $readingName, $readingVal, $attrNameRange, $attrNameDelay, $swayRangeLow, $swayRangeHigh, $swayDelayLow, $swayDelayHigh, $swayRangeLowVal, $swayRangeHighVal) = @_;
my ($swayRangeLowAttr, $swayRangeHighAttr) = split(':', AttrVal($hash->{NAME}, $attrNameRange, "$swayRangeLow:$swayRangeHigh"));
my ($swayDelayLowAttr, $swayDelayHighAttr) = split(':', AttrVal($hash->{NAME}, $attrNameDelay, "$swayDelayLow:$swayDelayHigh"));
my $swayValLast = exists($hash->{helper}{sway}{$readingName}) ? $hash->{helper}{sway}{$readingName} : $swayRangeLowVal;
my $swayVal = $swayValLast;
if (!defined($swayRangeLowAttr) && !defined($swayRangeHighAttr)) {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif (!defined($swayRangeLowAttr) && $swayRangeHighAttr eq '' || !defined($swayRangeHighAttr) && $swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif ($swayRangeHighAttr eq '' && $swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeLow;
$swayRangeHighAttr = $swayRangeHigh;
} elsif ($swayRangeLowAttr eq '') {
$swayRangeLowAttr = $swayRangeHighAttr;
} elsif ($swayRangeHighAttr eq '') {
$swayRangeHighAttr = $swayRangeLowAttr;
}
($swayRangeLowAttr, $swayRangeHighAttr) = ($swayRangeHighAttr, $swayRangeLowAttr) if ($swayRangeLowAttr > $swayRangeHighAttr);
if ($readingVal < $swayRangeLowAttr) {
$swayVal = $swayRangeLowVal;
} elsif ($readingVal >= $swayRangeHighAttr) {
$swayVal = $swayRangeHighVal;
} elsif ($readingVal >= $swayRangeLowAttr && $swayVal eq $swayRangeLowVal) {
$swayVal = $swayRangeLowVal;
} elsif ($readingVal < $swayRangeHighAttr && $swayVal eq $swayRangeHighVal) {
$swayVal = $swayRangeHighVal;
}
if (!defined($swayDelayLowAttr) && !defined($swayDelayHighAttr)) {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif (!defined($swayDelayLowAttr) && $swayDelayHighAttr eq '' || !defined($swayDelayHighAttr) && $swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif ($swayDelayHighAttr eq '' && $swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayLow;
$swayDelayHighAttr = $swayDelayHigh;
} elsif ($swayDelayLowAttr eq '') {
$swayDelayLowAttr = $swayDelayHighAttr;
} elsif ($swayDelayHighAttr eq '') {
$swayDelayHighAttr = $swayDelayLowAttr;
}
if ($swayVal eq $swayValLast) {
$hash->{helper}{sway}{$readingName} = $swayVal;
if (exists $hash->{helper}{timer}{sway}{$readingName}{delay}) {
# clear timer as sway reverses
RemoveInternalTimer($hash->{helper}{timer}{sway}{$readingName}{delay});
delete $hash->{helper}{timer}{sway}{$readingName}{delay};
}
} else {
$hash->{helper}{sway}{$readingName} = $swayValLast;
my $swayDelay = $swayVal eq $swayRangeHighVal ? $swayDelayHighAttr : $swayDelayLowAttr;
if (exists $hash->{helper}{timer}{sway}{$readingName}{delay}) {
$swayVal = $swayValLast;
} elsif ($swayDelay > 0) {
@{$hash->{helper}{timer}{sway}{$readingName}{delay}} = ($hash, $readingName, $swayVal, $swayDelay, 1, 5, 1);
InternalTimer(gettimeofday() + $swayDelay, 'ModbusElsnerWS_swayCtrlDelay', $hash->{helper}{timer}{sway}{$readingName}{delay}, 0);
$swayVal = $swayValLast;
}
}
return $swayVal;
}
sub ModbusElsnerWS_swayCtrlDelay($) {
my ($readingParam) = @_;
my ($hash, $readingName, $readingVal, $delay, $ctrl, $log, $clear) = @$readingParam;
if (defined $hash) {
readingsSingleUpdate($hash, $readingName, $readingVal, $ctrl);
Log3 $hash->{NAME}, $log, " ModbusElsnerWS " . $hash->{NAME} . " EVENT $readingName: $readingVal" if ($log);
$hash->{helper}{sway}{$readingName} = $readingVal;
delete $hash->{helper}{timer}{sway}{$readingName}{delay} if ($clear == 1);
}
return;
}
sub ModbusElsnerWS_readingsSingleUpdate($) {
my ($readingParam) = @_;
my ($hash, $readingName, $readingVal, $ctrl, $log, $clear) = @$readingParam;
if (defined $hash) {
readingsSingleUpdate($hash, $readingName, $readingVal, $ctrl);
delete $hash->{helper}{timer}{$readingName} if ($clear == 1);
#Log3 $hash->{NAME}, $log, " ModbusElsnerWS " . $hash->{NAME} . " EVENT $readingName: $readingVal" if ($log);
}
return;
}
sub ModbusElsnerWS_readingsBulkUpdate($$$$$$) {
my ($hash, $readingName, $readingVal, $theshold, $averageParam, $sFormat) = @_;
if (exists $hash->{helper}{timer}{heartbeat}) {
$readingVal = sprintf("$sFormat", ModbusElsnerWS_Smooting($hash, $readingName, $readingVal, $theshold, $averageParam));
readingsBulkUpdateIfChanged($hash, $readingName, $readingVal);
} else {
$readingVal = sprintf("$sFormat", ModbusElsnerWS_Smooting($hash, $readingName, $readingVal, 0, $averageParam));
readingsBulkUpdate($hash, $readingName, $readingVal);
}
return $readingVal;
}
#
sub ModbusElsnerWS_cdmClearTimer($) {
my ($functionArray) = @_;
my ($hash, $timer) = @$functionArray;
delete $hash->{helper}{timer}{$timer};
#Log3 $hash->{NAME}, 3, "ModbusElsnerWS $hash->{NAME} ModbusElsnerWS_cdmClearTimer $timer executed.";
return;
}
sub ModbusElsnerWS_Undef($$) {
my ($hash, $arg) = @_;
ModbusLD_Undef($hash, $arg);
return;
}
# Delete
sub ModbusElsnerWS_Delete($$) {
my ($hash, $name) = @_;
my $logName = "FileLog_$name";
my ($count, $gplotFile, $logFile, $weblinkName, $weblinkHash);
Log3 $name, 2, "ModbusElsnerWS $name deleted";
# delete FileLog device and log files
if (exists $defs{$logName}) {
$logFile = $defs{$logName}{logfile};
$logFile =~ /^(.*)($name).*\.(.*)$/;
$logFile = $1 . $2 . "*." . $3;
CommandDelete(undef, "FileLog_$name");
Log3 $name, 2, "ModbusElsnerWS FileLog_$name deleted";
$count = unlink glob $logFile;
Log3 $name, 2, "ModbusElsnerWS $logFile >> $count files deleted";
}
# delete SVG devices and gplot files
while (($weblinkName, $weblinkHash) = each(%defs)) {
if ($weblinkName =~ /^SVG_$name.*/) {
$gplotFile = "./www/gplot/" . $defs{$weblinkName}{GPLOTFILE} . "*.gplot";
CommandDelete(undef, $weblinkName);
Log3 $name, 2, "ModbusElsnerWS $weblinkName deleted";
$count = unlink glob $gplotFile;
Log3 $name, 2, "ModbusElsnerWS $gplotFile >> $count files deleted";
}
}
return undef;
}
1;
=pod
=item summary ModbusElsnerWS Elsner Weather Station P03/3-Modbus RS485 evaluation modul
=item summary_DE ModbusElsnerWS Elsner Wetterstation P03/3-Modbus RS485 Auswertemodul
=begin html
<a name="ModbusElsnerWS"></a>
<h3>ModbusElsnerWS</h3>
<ul>
The ModbusElsnerWS weather evaluation modul serves as a connecting link between the
<a href="https://www.elsner-elektronik.de/shop/de/produkte-shop/gebaeudetechnik-konventionell/modbus-sensoren/p03-3-modbus-747.html">Elsner P03/3-Modbus Weather Stations</a>
and blind actuators. ModbusElsnerWS uses the low level Modbus module to provide a way to communicate with the
<a href="https://www.elsner-elektronik.de/shop/de/produkte-shop/gebaeudetechnik-konventionell/modbus-sensoren/p03-3-modbus-747.html">Elsner P03/3-Modbus Weather Stations</a>.
It read the modbus holding registers for the different values and process
them in a periodic interval. It evaluates the received weather data and based on adjustable thresholds and delay
times it generates up/down signals for blinds according to wind, rain and sun. This way blinds can be pilot-controlled
and protected against thunderstorms. The GPS function of the sensor provides the current values of the date, time,
sun azimuth, sun elevation, longitude and latitude.<br>
As an alternative to the module for the Elsner Modbus sensors, sensors with the manufacturer-specific serial
RS485 protocol can also be used. The <a href="#ElsnerWS">ElsnerWS</a> module is available for this purpose. EnOcean profiles
"Environmental Applications" with the EEP A5-13-01 ... EEP A5-13-06 are also available for these weather stations,
but they also require weather evaluation units from Eltako or AWAG, for example. The functional scope of
the modules is widely similar.
<br><br>
<b>Functions</b>
<ul>
<li>Evaluation modul for the weather sensors P03/3-Modbus and P03/3-Modbus GPS</li>
<li>Processing weather raw data and creates graphic representation</li>
<li>Alarm signal in case of failure of the weather sensor</li>
<li>Up/down readings for blinds according to wind, rain and sun</li>
<li>Adjustable switching thresholds and delay times</li>
<li>Day/night signal</li>
<li>Display of date, time, sun azimuth, sun elevation, longitude and latitude</li>
</ul><br>
<b>Prerequisites</b>
<ul>
This module requires the basic <a href="#Modbus">Modbus</a> module which itsef requires Device::SerialPort
or Win32::SerialPort module.
</ul><br>
<b>Hardware Connection</b>
<ul>
The weather sensor P03/3-Modbus(GPS) is connected via a shielded cable 2x2x0.5 mm2 to a RS485 transceiver.
The sensor is connected via the pins A to the RS485 B(+)-Port, B to RS485 A(-)-Port, 1 to + 12...28 V, 2 to GND and Shield.
Please note that the RS485 connection names are reversed. Only the usual pins for serial Modbus communication A, B and Ground are needed.<br>
The serial bus should be terminated at its most remote ends with 120 Ohm resistors. If several Modbus units are connected to
the serial bus, only the termination resistor in the devices furthest ends must be switched on.<br>
More information about the sensor, see
<a href="https://www.elsner-elektronik.de/shop/de/fileuploader/download/download/?d=1&file=custom%2Fupload%2F30146-30147_P033-Modbus_P033-Modbus-GPS_Datenblatt2-0_19Mar18_DBEEA6010.pdf">P03/3-Modbus(GPS) User Guide</a>.<br>
The USB adapters
<a href="https://www.digitus.info/produkte/computer-zubehoer-und-komponenten/computer-zubehoer/seriell-und-parallel-adapter/da-70157/">Digitus DA-70157</a>,
<a href="https://shop.in-circuit.de/product_info.php?cPath=33&products_id=81">In-Circuit USB-RS485-Bridge</a>
and <a href="http://www.dsdtech-global.com/2018/01/sh-u10-spp.html">DSD TECH SH-U10 USB to RS485 converter</a>
are successfully tested at a Raspberry PI in conjunction with the weather sensor.
</ul><br>
<a name="ModbusElsnerWSDefine"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; ModbusElsnerWS id=&lt;ID&gt; interval=&lt;Interval&gt;|passive</code><br><br>
The module connects to the Elsner Weather Station with the Modbus Id &lt;ID&gt; through an already defined Modbus device
and actively requests data from the system every &lt;Interval&gt; seconds. The query interval should be set to 1 second.<br>
The following parameters apply to the default factory settings and an RS485 transceiver to USB.
<br><br>
Example:<br>
<code>define modbus Modbus /dev/ttyUSB1@19200,8,E,1</code><br>
<code>define WS ModbusElsnerWS id=1 interval=1</code>
</ul><br>
<a name="ModbusElsnerWSget"></a>
<b>Get</b>
<ul>
<code>get &lt;name&gt; &lt;value&gt;</code>
<br><br>
<ul>
where <code>value</code> is
<li>raw<br>
get sensor dates manualy</li>
</ul>
</ul><br>
<a name="ModbusElsnerWSattr"></a>
<b>Attributes</b>
<ul>
<ul>
<li><a name="ModbusElsnerWS_brightnessDayNight">brightnessDayNight</a> E_min/lx:E_max/lx,
[brightnessDayNight] = 0...99000:0...99000, 10:20 is default.<br>
Set switching thresholds for reading dayNight based on the reading brightness.
</li>
<li><a name="ModbusElsnerWS_brightnessDayNightDelay">brightnessDayNightDelay</a> t_reset/s:t_set/s,
[brightnessDayNightDelay] = 0...99000:0...99000, 600:600 is default.<br>
Set switching delay for reading dayNight based on the reading brightness. The reading dayNight is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_brightnessSunny">brightnessSunny</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunny based on the reading brightness.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnyDelay">brightnessSunnyDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunny based on the reading brightness. The reading isSunny is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnyEast">brightnessSunnyEast</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnyEast based on the reading sunEast.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnyEastDelay">brightnessSunnyEastDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnyEast based on the reading sunEast. The reading isSunnyEast is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnySouth">brightnessSunnySouth</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnySouth based on the reading sunSouth.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnySouthDelay">brightnessSunnySouthDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnySouth based on the reading sunSouth. The reading isSunnySouth is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnyWest">brightnessSunnyWest</a> E_min/lx:E_max/lx,
[brightnessSunny] = 0...99000:0...99000, 20000:40000 is default.<br>
Set switching thresholds for reading isSunnyWest based on the reading sunWest.
</li>
<li><a name="ModbusElsnerWS_brightnessSunnyWestDelay">brightnessSunnyWestDelay</a> t_reset/s:t_set/s,
[brightnessSunnyDelay] = 0...99000:0...99000, 120:30 is default.<br>
Set switching delay for reading isSunnyWest based on the reading sunWest. The reading isSunnyWest is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
<li><a name="ModbusElsnerWS_timeEvent">timeEvent</a> no|yes,
[timeEvent] = no|yes, no is default.<br>
Update the reading time periodically.
</li>
<li><a name="ModbusElsnerWS_windSpeedStormy">windSpeedStormy</a> v_min/m/s:v_max/m/s,
[windSpeedStormy] = 0...35:0...35, 13.9:17.2 is default.<br>
Set switching thresholds for reading isStormy based on the reading windSpeed.
</li>
<li><a name="ModbusElsnerWS_windSpeedStormyDelay">windSpeedStormyDelay</a> t_reset/s:t_set/s,
[windSpeedStormyDelay] = 0...99000:0...99000, 60:3 is default.<br>
Set switching delay for reading isStormy based on the reading windSpeed. The reading isStormy is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_windSpeedWindy">windSpeedWindy</a> v_min/m/s:v_max/m/s,
[windSpeedWindy] = 0...35:0...35, 1.6:3.4 is default.<br>
Set switching thresholds for reading isWindy based on the reading windSpeed.
</li>
<li><a name="ModbusElsnerWS_windSpeedWindyDelay">windSpeedWindyDelay</a> t_reset/s:t_set/s,
[windSpeedWindyDelay] = 0...99000:0...99000, 60:3 is default.<br>
Set switching delay for reading isWindy based on the reading windSpeed. The reading isWindy is reset or set
if the thresholds are permanently undershot or exceed during the delay time.
</li>
<li><a name="ModbusElsnerWS_updateGlobalAttr">updateGlobalAttr</a> no|yes,
[timeEvent] = no|yes, no is default.<br>
Update the global attributes latitude and longitude with the received GPS coordinates.
</li>
</ul>
</ul><br>
<a name="ModbusElsnerWSevents"></a>
<b>Generated events</b>
<ul>
<ul>
<li>T: t/&#176C B: E/lx W: v/m/s IR: no|yes</li>
<li>brightness: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>date: JJJJ-MM-TT</li>
<li>dayNight: day|night</li>
<li>hemisphere: north|south</li>
<li>isRaining: no|yes</li>
<li>isStormy: no|yes</li>
<li>isSunny: no|yes</li>
<li>isSunnyEast: no|yes</li>
<li>isSunnySouth: no|yes</li>
<li>isSunnyWest: no|yes</li>
<li>isWindy: no|yes</li>
<li>latitude: &phi;/&deg; (Sensor Range: &phi; = -90 &deg; ... 90 &deg;)</li>
<li>longitude: &lambda;/&deg; (Sensor Range: &lambda; = -180 &deg; ... 180 &deg;)</li>
<li>sunAzimuth: &alpha;/&deg; (Sensor Range: &alpha; = 0 &deg; ... 359 &deg;)</li>
<li>sunEast: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>sunElevation: &beta;/&deg; (Sensor Range: &beta; = -90 &deg; ... 90 &deg;)</li>
<li>sunSouth: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>sunWest: E/lx (Sensor Range: E = 0 lx ... 99000 lx)</li>
<li>temperature: t/&#176C (Sensor Range: t = -40 &#176C ... 70 &#176C)</li>
<li>time: hh:mm:ss</li>
<li>timeZone: CET|CEST|UTC</li>
<li>twilight: T/% (Sensor Range: T = 0 % ... 100 %)</li>
<li>windSpeed: v/m/s (Sensor Range: v = 0 m/s ... 70 m/s)</li>
<li>windStrength: B (Sensor Range: B = 0 Beaufort ... 12 Beaufort)</li>
<li>state: T: t/&#176C B: E/lx W: v/m/s IR: no|yes</li>
</ul>
</ul>
</ul>
=end html
=cut