fhem-mirror/FHEM/10_WS980.pm
2021-07-11 12:24:22 +00:00

1630 lines
55 KiB
Perl

########################################################################################
#
# WS980.pm
#
# FHEM module for WS980-Wifi Weather Station
#
# Christian Hoenig
#
# $Id$
#
########################################################################################
#
# This programm 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.
#
# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html.
# A copy is found in the textfile GPL.txt and important notices to the license
# from the author is found in LICENSE.txt distributed with these scripts.
#
# This script 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.
#
########################################################################################
package main;
use strict;
use warnings;
use IO::Socket::INET;
use POSIX qw(strftime);
my $version = "1.5.0";
#------------------------------------------------------------------------------------------------------
# global constants
#------------------------------------------------------------------------------------------------------
use constant REQUESTS => {
"firmware" => { # \xff\xff\x50\x03\x53
"type" => "\x50",
"value" => {
"name" => "firmware",
"width" => "auto",
}
},
"current" => { # \xff\xff\x0b\x00\x06\x04\x04\x19
"type" => "\x0b",
"subtype" => "\x04",
},
"historyMax" => { # \xff\xff\x0b\x00\x06\x05\x05\x1b,
"type" => "\x0b",
"subtype" => "\x05",
"postfix" => "_historyMax",
},
"historyMin" => { # \xff\xff\x0b\x00\x06\x06\x06\x1d
"type" => "\x0b",
"subtype" => "\x06",
"postfix" => "_historyMin",
},
"todayMax" => { # \xff\xff\x0b\x00\x06\x07\x07\x1f
"type" => "\x0b",
"subtype" => "\x07",
"postfix" => "_todayMax",
},
"todayMin" => { #\xff\xff\x0b\x00\x06\x08\x08\x21
"type" => "\x0b",
"subtype" => "\x08",
"postfix" => "_todayMin",
},
};
use constant HAS_TIME => 0x40;
use constant HAS_DATE => 0x80;
use constant VALUES => {
0x01 => {"name" => "temperatureInside", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "°C" , "error" => 0x7FFF }, # °C ## x / 10.0 - 40.0
0x02 => {"name" => "temperature", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "°C" , "error" => 0x7FFF }, # °C ## x / 10.0 - 40.0
0x03 => {"name" => "dewPoint", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "°C" , "error" => 0x7FFF }, # °C ## x / 10.0 - 40.0
0x04 => {"name" => "windChill", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "°C" , "error" => 0x7FFF }, # °C ## x / 10.0 - 40.0
0x05 => {"name" => "heatIndex", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "°C" , "error" => 0x7FFF }, # °C ## x / 10.0 - 40.0
0x06 => {"name" => "humidityInside", "bytes" => 1, "factor" => 1, "format" => "%d" , "unit" => "%" , "error" => 0xFF }, # %
0x07 => {"name" => "humidity", "bytes" => 1, "factor" => 1, "format" => "%d" , "unit" => "%" , "error" => 0xFF }, # %
0x08 => {"name" => "pressureAbs", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "hPa" , "error" => 0xFFFF }, # hPa err=?
0x09 => {"name" => "pressureRel", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "hPa" , "error" => 0xFFFF }, # hPa err=?
0x0A => {"name" => "windDirection", "bytes" => 2, "factor" => 1, "format" => "%d" , "unit" => "deg" , "error" => 0x0FFF }, # °
0x0B => {"name" => "wind", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "m/s" , "error" => 0xFFFF }, # m/s
0x0C => {"name" => "windGusts", "bytes" => 2, "factor" => 10, "format" => "%.1f", "unit" => "m/s" , "error" => 0xFFFF }, # m/s
0x0D => {"name" => "rainEvent", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x0E => {"name" => "rainRate", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x0F => {"name" => "rainPerHour", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # err=?
0x10 => {"name" => "rainPerDay", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x11 => {"name" => "rainPerWeek", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x12 => {"name" => "rainPerMonth", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x13 => {"name" => "rainPerYear", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x14 => {"name" => "rainTotal", "bytes" => 4, "factor" => 10, "format" => "%.1f", "unit" => "mm" , "error" => 0xFFFFFFFF}, # mm err=?
0x15 => {"name" => "brightness", "bytes" => 4, "factor" => 10, "format" => "%d" , "unit" => "lux" , "error" => 0x00FFFFFF}, # lux
0x16 => {"name" => "uv", "bytes" => 2, "factor" => 1, "format" => "%d" , "unit" => "uW/m^2", "error" => 0xFFFF }, # uW/m^2
0x17 => {"name" => "uvIndex", "bytes" => 1, "factor" => 1, "format" => "%d" , "unit" => "uvi" , "error" => 0xFF }, # 0-15 index ??
};
use constant UNIT_CONVERSIONS => {
"°C" => {
"attr" => "unit_temperature", # °C °F
"fnc" => {
"°C" => sub { my ($c) = @_; return $c },
"°F" => sub { my ($c) = @_; return 9/5 * $c + 32 },
},
},
"hPa" => {
"attr" => "unit_pressure", # hPa inHg mmHg
"fnc" => {
"hPa" => sub { my ($c) = @_; return $c },
"inHg" => sub { my ($c) = @_; return $c * 0.75006375541921 / 10 / 2.54},
"mmHg" => sub { my ($c) = @_; return $c * 0.75006375541921},
},
},
"m/s" => {
"attr" => "unit_wind", # m/s km/h knot mph bft
"fnc" => {
"m/s" => sub { my ($c) = @_; return $c },
"km/h" => sub { my ($c) = @_; return $c * 3.6 },
"knot" => sub { my ($c) = @_; return $c * 1.943844 },
"mph" => sub { my ($c) = @_; return $c * 2.236936 },
"bft" => sub { my ($c) = @_; return ($c / 0.836) ** (2/3) },
},
},
"mm" => {
"attr" => "unit_rain", # mm in
"fnc" => {
"mm" => sub { my ($c) = @_; return $c },
"in" => sub { my ($c) = @_; return $c / 10 / 2.54 },
},
},
"lux" => {
"attr" => "unit_light", # lux fc w/m^2
"fnc" => {
"lux" => sub { my ($c) = @_; return $c },
"fc" => sub { my ($c) = @_; return $c * 0.09290304000008},
"w/m^2" => sub { my ($c) = @_; return $c * 0.0079}, # 0.001464128843338 @ 555nm
},
},
};
#------------------------------------------------------------------------------------------------------
# Initialize
#------------------------------------------------------------------------------------------------------
sub WS980_Initialize($)
{
my ($hash) = @_;
WS980_Log(undef, 5, "called");
$hash->{DefFn} = "WS980_DefFn";
$hash->{UndefFn} = "WS980_UndefFn";
$hash->{AttrFn} = "WS980_AttrFn";
$hash->{SetFn} = "WS980_SetFn";
#$hash->{GetFn} = "WS980_GetFn";
$hash->{ReadFn} = "WS980_ReadFn";
$hash->{WriteFn} = "WS980_WriteFn";
$hash->{AttrList} = "altitude ".
"events:textField-long ".
"connection:Keep-Alive,Close ".
"invalidValues:updateReading,skip,skipAndLog ".
"requests:multiple-strict,".join(",", sort keys %{REQUESTS()})." ".
"showRawBuffer:1 ".
"silentReconnect:1 ".
"disable:1 ".
WS980_extractAttrsFromUnits() .
$readingFnAttributes;
foreach my $d (sort keys %{$modules{WS980}{defptr}}) {
my $hash = $modules{WS980}{defptr}{$d};
# update version in devices
$hash->{VERSION} = $version;
# initialize PORT if not done yet - PORT was introduced in 0.12.0
if (!defined($hash->{PORT})) {
$hash->{PORT} = 45000;
}
$hash->{helper}{requestInProgress} = 0;
}
}
#------------------------------------------------------------------------------------------------------
# Define
#------------------------------------------------------------------------------------------------------
sub WS980_DefFn($$)
{
my ( $hash, $def ) = @_;
my @a = split( "[ \t]+", $def );
splice( @a, 1, 1 );
# check syntax
if(int(@a) < 1) {
return "Wrong syntax: use define <name> WS980 [IP] [INTERVAL]";
}
my ($name, $ip, $interval) = @a;
WS980_Log($hash, 5, "called");
my $port = 45000;
# try to auto-discover the IP
if (!defined($ip)) {
($ip, $port) = WS980_autodiscoverIP($hash);
}
return "Autodiscovery failed, please set IP by hand" unless (defined($ip));
$hash->{IP} = $ip;
$hash->{PORT} = $port;
$hash->{VERSION} = $version;
$hash->{INTERVAL} = $interval ? $interval : 30;
$hash->{helper}{requestInProgress} = 0;
$modules{WS980}{defptr}{$hash->{IP}} = $hash;
my $eventsCfg = AttrVal($name, "events", "");
if ($eventsCfg ne "") {
WS980_parseEventsAttr($hash, $eventsCfg)
}
if ($init_done) {
InternalTimer( gettimeofday()+0, "WS980_updateValues", $hash);
} else {
InternalTimer( gettimeofday()+10, "WS980_updateValues", $hash);
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# Undefine
#------------------------------------------------------------------------------------------------------
sub WS980_UndefFn($$)
{
my ($hash, $arg) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "called");
delete $modules{WS980}{defptr}{$hash->{IP}};
RemoveInternalTimer($hash);
WS980_Close($hash);
return undef;
}
#------------------------------------------------------------------------------------------------------
# AttrFn
#------------------------------------------------------------------------------------------------------
sub WS980_AttrFn(@)
{
my ($cmd, $name, $attrName, $attrVal) = @_;
my $hash = $defs{$name};
##################
#### altitude ####
if ($attrName eq "altitude") {
if ($cmd eq "set") {
my $isNumeric = ($attrVal eq $attrVal+0);
if ($isNumeric) {
WS980_updateRelPressure($hash);
return undef;
} else {
return "'altitude' must be a numeric value";
}
}
}
####################
#### connection ####
if ($attrName eq "connection") {
if ($cmd eq "set") {
if ($attrVal eq "Keep-Alive") {
return undef;
}
elsif ($attrVal eq "Close") {
WS980_Close($hash);
return undef;
}
else {
return "'connection' must be either Keep-Alive or Close";
}
}
}
#######################
#### invalidValues ####
if ($attrName eq "invalidValues") {
if ($cmd eq "set") {
if ($attrVal eq "updateReading" || $attrVal eq "skip" || $attrVal eq "skipAndLog") {
return undef;
}
else {
return "'invalidValues' must be either updateReading, skip or skipAndLog";
}
}
}
################
#### events ####
if ($attrName eq "events") {
if ($cmd eq "set") {
WS980_parseEventsAttr($hash, $attrVal);
} elsif ($cmd eq "del") {
WS980_parseEventsAttr($hash, undef);
}
}
##################
#### requests ####
if ($attrName eq "requests") {
if ($cmd eq "set") {
my @parts = split(/[, ]/, $attrVal);
foreach my $part (@parts) {
if (!defined REQUESTS->{$part}) {
return "Invalid 'requests'-type: $part";
}
}
}
}
#################
#### disable ####
if ($attrName eq "disable") {
if ($cmd eq "set" and $attrVal eq "1") {
WS980_Close($hash);
readingsSingleUpdate ( $hash, "state", "disabled", 1 );
WS980_Log($hash, 2, "disabled");
}
elsif ($cmd eq "del") {
readingsSingleUpdate ( $hash, "state", "active", 1 );
WS980_Log($hash, 2, "enabled");
}
}
#######################
#### showRawBuffer ####
if ($attrName eq "showRawBuffer") {
if ($cmd eq "set" and $attrVal eq "1") {
# nothing here :)
}
elsif ($cmd eq "del") {
CommandDeleteReading(undef, "$name rawBuffer.*");
}
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# SetFn
#------------------------------------------------------------------------------------------------------
sub WS980_SetFn($$@)
{
my ($hash, $name, @aa) = @_;
my ($cmd, @args) = @aa;
if ($cmd eq "?") {
return "Unknown argument $cmd, choose one of " if WS980_isDisabled($hash);
}
if ($cmd eq 'update') {
return "usage: update" if( @args != 0 );
WS980_updateValues($hash);
return undef;
}
my $list = "update:noArg";
return "Unknown argument $cmd, choose one of $list";
}
#------------------------------------------------------------------------------------------------------
# request values from WS980 device
#------------------------------------------------------------------------------------------------------
sub WS980_autodiscoverIP($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "called");
my $socket = IO::Socket::INET->new(
PeerAddr => inet_ntoa(INADDR_BROADCAST),
Broadcast => 1,
ReusAddr => 1,
ReusePort => 1,
PeerPort => '46000',
Proto => 'udp',
Type => SOCK_DGRAM
);
if (!$socket) {
WS980_Log($hash, 1, "autodiscovery failed: no socket");
return (undef,undef);
}
my $recvSocket = IO::Socket::INET->new(
Proto => 'udp',
LocalPort => $socket->sockport(),
ReusAddr => 1,
ReusePort => 1,
);
if (!$recvSocket) {
WS980_Log($hash, 1, "autodiscovery failed: no recvSocket");
return (undef,undef);
}
# set receive timeout to 500msecs second (format is: secs, microsecs)
if (!$recvSocket->setsockopt(SOL_SOCKET, SO_RCVTIMEO, pack('l!l!', 0, 500*1000))) {
WS980_Log($hash, 1, "autodiscovery failed: could not set SO_RCVTIMEO on recvSocket");
return (undef,undef);
}
# Broadcast auf 255.255.255.255:46000
# -> ffff12000416
my $req = WS980_createRequestRaw("\x12");
# send request
WS980_Log($hash, 4, "broadcasting auto-discovery: " . WS980_hexDump($req));
if ($socket->send($req) == 0) {
WS980_Log($hash, 1, "autodiscovery failed: cannot send request");
return (undef,undef);
}
$socket->close();
# receive a response of up to 10240 characters from server
my $rawbuf;
$recvSocket->recv($rawbuf, 10240);
$recvSocket->close();
# ffff 12 LLLL ?? ?? ?? ?? ?? ?? I1 I2 I3 I4 PPPP LN NN..NN C2
# 84 f3 eb 21 8c d1
WS980_Log($hash, 4, "received raw reply: " . WS980_hexDump($rawbuf));
my ($typeStr, $buf) = WS980_handleReply($hash, $rawbuf);
if (defined($buf)) {
my ($ip1, $ip2, $ip3, $ip4, $port, $stationName) = unpack("x[6]CCCCnC/A", $buf);
WS980_Log($hash, 2, "autodiscovery-reply: $ip1, $ip2, $ip3, $ip4, $port, $stationName");
return (sprintf("%d.%d.%d.%d", $ip1, $ip2, $ip3, $ip4), $port);
}
else
{
WS980_Log($hash, 1, "autodiscovery failed: looks like the reply could not be decoded");
return (undef,undef);
}
}
#------------------------------------------------------------------------------------------------------
# request values from WS980 device
#------------------------------------------------------------------------------------------------------
sub WS980_updateValues($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $ip = $hash->{IP};
WS980_Log($hash, 5, "called");
my $interval = $hash->{INTERVAL};
RemoveInternalTimer($hash, "WS980_updateValues");
InternalTimer(gettimeofday()+$interval, "WS980_updateValues", $hash);
return undef if (WS980_isDisabled($hash));
if ($hash->{helper}{requestInProgress} == 1) {
my $logLevel = AttrVal($name, "silentReconnect", "") eq "1" ? 4 : 3;
WS980_Log($hash, $logLevel, "looks like the last request did not receive an answer, trying to reconnect");
WS980_Close($hash);
}
my @activeRequests = split(/[, ]/, AttrVal($name, "requests", ""));
if (!@activeRequests) {
@activeRequests = keys %{REQUESTS()};
}
$hash->{helper}{activeRequests} = \@activeRequests;
my $ok = WS980_Open($hash);
WS980_writeNextActiveRequest($hash) if ($ok);
return undef;
}
#------------------------------------------------------------------------------------------------------
# takes the next request from @activeRequests and sends it to the WS980
#------------------------------------------------------------------------------------------------------
sub WS980_writeNextActiveRequest($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "activeRquests: " . join(" ", @{$hash->{helper}{activeRequests}}));
my $valueType = shift(@{$hash->{helper}{activeRequests}});
if (!defined($valueType)) {
my $datestring = strftime "%F %T", localtime;
readingsSingleUpdate($hash, "lastUpdate", "$datestring", 1);
if (AttrVal($name, "connection", "Keep-Alive") eq "Close") {
WS980_Close($hash);
}
return;
}
my $buf = WS980_createRequest($hash, $valueType);
if (defined $buf) {
my $logLevel = AttrVal($name, "silentReconnect", "") eq "1" ? 4 : 3;
WS980_Log($hash, $logLevel, "Sending new request for '$valueType'...");
WS980_WriteFn($hash, $buf);
} else {
WS980_Close($hash);
}
}
#------------------------------------------------------------------------------------------------------
# update multiple 'values'
#------------------------------------------------------------------------------------------------------
sub WS980_handleMultiValuesUpdate($$$)
{
my ($hash, $valueType, $buf) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "decoding block: " . WS980_hexDump($buf));
my $showRawBuffer = AttrVal($name, "showRawBuffer", "0") eq "1" ? 1 : 0;
my $invalidValues = AttrVal($name, "invalidValues", "updateReading");
for (my $i = 0; $i < length($buf); )
{
my $id = unpack("x[$i] C", $buf);
$i += 1;
my $hasDate = ($id & HAS_DATE);
my $hasTime = ($id & HAS_TIME);
$id = $id & ~HAS_DATE & ~HAS_TIME;
if (!(exists VALUES->{$id})) {
my $decs = "";
my $hexs = "";
for (my $j = $i; $j < length($buf); $j++) {
my $hex = WS980_binToHex(substr($buf,$j,1));
$decs .= sprintf("%d ", hex($hex));
$hexs .= sprintf("%s ", $hex);
}
WS980_error($hash, sprintf("%d not found\n[%s]\n[%s]", $id, $decs, $hexs));
last;
}
my $reading = VALUES->{$id}{"name"};
if (defined(REQUESTS->{$valueType}{"prefix"})) {
$reading = REQUESTS->{$valueType}{"prefix"} . $reading;
};
if (defined(REQUESTS->{$valueType}{"postfix"})) {
$reading .= REQUESTS->{$valueType}{"postfix"};
};
my $bytes = VALUES->{$id}{"bytes"};
my $factor = VALUES->{$id}{"factor"};
my $format = VALUES->{$id}{"format"};
my $unit = VALUES->{$id}{"unit"};
my $errVal = VALUES->{$id}{"error"};
my $rawValue = substr($buf, $i, $bytes);
$i += $bytes;
my $value = hex(WS980_binToHex($rawValue));
if ($value eq $errVal) {
if ($invalidValues eq "updateReading") {
$value = 'n/a';
} else {
WS980_Log($hash, 4, "skipping invalid value of \"$reading\"") if ($invalidValues eq "skipAndLog");
next;
}
} else {
# convert negative values
my $lbit = 1 << ($bytes * 2 * 4) - 1;
if ($value & $lbit) {
$value = $value - ($lbit << 1);
}
# respect $factor
$value = $value / $factor;
# handle unit conversion
if (defined(UNIT_CONVERSIONS->{$unit})) {
my $newUnit = AttrVal($name, UNIT_CONVERSIONS->{$unit}{attr}, $unit);
if ($newUnit ne $unit) {
if (defined(UNIT_CONVERSIONS->{$unit}{"fnc"}{$newUnit})) {
my $fnc = UNIT_CONVERSIONS->{$unit}{"fnc"}{$newUnit};
$value = $fnc->($value);
}
}
}
# and format
$value = sprintf($format, $value)
}
# append raw if ($showRawBuffer)
$value .= " " . WS980_hexDump($rawValue) if ($showRawBuffer);
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime();
if ($hasDate) {
($year, $mon, $mday) = unpack("x[$i] CCC", $buf);
$year += 2000;
$i += 1 + 1 + 1; # 2000+year + month + day
WS980_error($hash, sprintf("hasDate is not handeled"));
}
if ($hasTime) {
($hour, $min) = unpack("x[$i] CC", $buf);
$i += 1 + 1; # hour + minute
}
my $ts;
if ($hasDate || $hasTime) {
$ts = sprintf("%04d-%02d-%02d %02d:%02d:00", $year+1900, $mon+1, $mday, $hour, $min);
}
readingsBeginUpdate($hash);
if ($hasDate || $hasTime) {
$hash->{".updateTimestamp"} = $ts;;
}
my $offset = $#{ $hash->{CHANGED} };
readingsBulkUpdate($hash, $reading, $value, 1);
if (($hasDate || $hasTime) && ($#{ $hash->{CHANGED} } != $offset)) {
# only add ts if there is a event to
$hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts;
}
readingsEndUpdate($hash,1);
}
}
#------------------------------------------------------------------------------------------------------
# update a single 'value'
#------------------------------------------------------------------------------------------------------
sub WS980_handleSingleValuesUpdate($$$)
{
my ($hash, $valueType, $buf) = @_;
my $name = $hash->{NAME};
my $len = REQUESTS->{$valueType}{"value"}{"width"};
if ($len eq "auto") {
my $value = unpack("C/A", $buf);
readingsSingleUpdate($hash, REQUESTS->{$valueType}{"value"}{"name"}, $value, 1);
} else {
WS980_error($hash, "cannot decode value in WS980_handleSingleValuesUpdate: " . $valueType);
}
}
#------------------------------------------------------------------------------------------------------
# * if disabled -> ...
#------------------------------------------------------------------------------------------------------
sub WS980_isDisabled($)
{
my ($hash) = @_;
return AttrVal($hash->{NAME}, "disable", "0") eq "1" ||
IsDisabled($hash->{NAME});
}
#------------------------------------------------------------------------------------------------------
# creates the binary request buffer for $valueType from %{REQUESTS()}
#------------------------------------------------------------------------------------------------------
sub WS980_createRequest($$)
{
my ($hash, $valueType) = @_;
my $name = $hash->{NAME};
if (!defined REQUESTS->{$valueType}) {
WS980_error($hash, "WS980_createRequest failed to create request: no config for $valueType");
return undef;
}
if (defined REQUESTS->{$valueType}{"req"}) {
return REQUESTS->{$valueType}{"req"};
}
return WS980_createRequestRaw(
defined(REQUESTS->{$valueType}{"type"}) ? REQUESTS->{$valueType}{"type"} : undef,
defined(REQUESTS->{$valueType}{"subtype"}) ? REQUESTS->{$valueType}{"subtype"} : undef,
defined(REQUESTS->{$valueType}{"data"}) ? REQUESTS->{$valueType}{"data"} : undef
);
}
#------------------------------------------------------------------------------------------------------
# creates the binary request buffer for $type, $subtype and $data
#------------------------------------------------------------------------------------------------------
sub WS980_createRequestRaw($;$$)
{
my ($type, $subtype, $data) = @_;
$type = "" if (!defined($type));
$subtype = "" if (!defined($subtype));
$data = "" if (!defined($data));
if ($type eq "\x0b") {
# ffff 0b LLLL XX C1 C2
my $cmd = $subtype . $data;
my $c1 = WS980_calculateChecksum($cmd);
my $len = 1 + 2 + length($cmd) + 1 + 1; # $type + LLLL + $cmd + $c1 + $c2
my $req = $type . pack("n", $len) . $cmd . pack("C", $c1);
my $c2 = WS980_calculateChecksum($req);
return "\xff\xff" . $req . pack("C", $c2);
}
elsif ($type eq "\x12") {
# ffff 12 LLLL C2
my $len = 1 + 2 + 1; # $type + LLLL + $c2
my $req = $type . pack("n", $len);
my $c2 = WS980_calculateChecksum($req);
return "\xff\xff" . $req . pack("C", $c2);
}
elsif ($type eq "\x50") {
# ffff 50 LL C2
my $len = 1 + 1 + 1; # $type + LL + $c2
my $req = $type . pack("C", $len);
my $c2 = WS980_calculateChecksum($req);
return "\xff\xff" . $req . pack("C", $c2);
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# parses the reply from the WS980
#------------------------------------------------------------------------------------------------------
sub WS980_handleReply($$)
{
my ($hash, $buf) = @_;
my $name = $hash->{NAME};
# remove leading 'ffff'
if ($buf =~ /^\xff\xff/) {
$buf = substr($buf, 2);
} else {
WS980_error($hash, "msg did not start with ffff");
return (undef, undef);
}
my $typeStr = "";
my $type = substr($buf, 0, 1);
if ($type eq "\x0b") {
# 0b has a checksum and a 2 byte length field
if (!WS980_checkChecksum($buf)) {
WS980_error($hash, "first checksum did not match");
return (undef, undef);
}
# remove $type
$buf = substr($buf, 1);
# check and remove the 2 byte length-field
my $len = unpack("n", $buf);
if ($len != length($buf) + 1) { # incl $type
WS980_error($hash, "length did not match: " . sprintf("%02x vs %02x", $len, length($buf)+1));
return (undef, undef);
}
$buf = substr($buf, 2);
# remove the first checksum
$buf = substr($buf, 0, -1);
# check the second checksum
if (!WS980_checkChecksum($buf)) {
WS980_error($hash, "second checksum did not match");
return (undef, undef);
}
# remove the second checksum
$buf = substr($buf, 0, -1);
# the next byte encodes the subtype (04, 05, 06, ...)
my $subType = substr($buf, 0, 1);
$typeStr = WS980_findConfigKey($type, $subType);
$buf = substr($buf, 1);
}
elsif ($type eq "\x12") { # autodiscovery
# 12 has a checksum and a 2 byte length field
if (!WS980_checkChecksum($buf)) {
WS980_error($hash, "first checksum did not match");
return (undef, undef);
}
# remove $type
$buf = substr($buf, 1);
# check and remove the 2 byte length-field
my $len = unpack("n", $buf);
if ($len != length($buf) + 1 + 2) { # incl $type + FFFF
WS980_error($hash, "length did not match: " . sprintf("%02x vs %02x", $len, length($buf)+3));
return (undef, undef);
}
$buf = substr($buf, 2);
}
elsif ($type eq "\x50") { # firmware
# remove '50'
$buf = substr($buf, 1);
$typeStr = WS980_findConfigKey($type);
# check and remove length-field
my $len = unpack("C", $buf);
if ($len != length($buf) + 1) {
# 50 is missing the checksum, so the length check fails
# WS980_error($hash, "length did not match: " . sprintf("%02x vs %02x", $len, length($buf)+3));
# return undef;
}
$buf = substr($buf, 1);
}
return ($typeStr, $buf);
}
#------------------------------------------------------------------------------------------------------
# returns the configKey for the given type and subtype (0b, 04 -> 'current')
#------------------------------------------------------------------------------------------------------
sub WS980_findConfigKey($;$)
{
my ($type, $subType) = @_; # bytearrays
foreach my $key (keys %{REQUESTS()}) {
if (defined(REQUESTS->{$key}{"type"}) && REQUESTS->{$key}{"type"} eq $type) {
if (!defined($subType) && !defined(REQUESTS->{$key}{"subtype"})) {
return $key;
}
if (defined(REQUESTS->{$key}{"subtype"}) && REQUESTS->{$key}{"subtype"} eq $subType) {
return $key;
}
}
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# opens a connection to IP:PORT
#------------------------------------------------------------------------------------------------------
sub WS980_Open($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $ip = $hash->{IP};
my $port = $hash->{PORT};
my $timeout = 0.25;
return 1 if ($hash->{CD});
my $logLevel = AttrVal($name, "silentReconnect", "") eq "1" ? 4 : 3;
WS980_Log($hash, $logLevel, "Creating socket connection to $ip:$port");
my $socket = new IO::Socket::INET(
PeerAddr => $ip,
PeerPort => $port,
Proto => 'tcp',
Timeout => $timeout,
);
if (!$socket) {
$hash->{ConnectionState} = 'disconnected';
WS980_error($hash, "Couldn't connect to $ip:$port: " . $@);
return 0;
}
# set receive timeout to 500msecs second (format is: secs, microsecs)
if (!$socket->setsockopt(SOL_SOCKET, SO_RCVTIMEO, pack('l!l!', 0, 500*1000))) {
WS980_error($hash, "Could not set SO_RCVTIMEO on socket");
return 0;
}
$hash->{FD} = $socket->fileno();
$hash->{CD} = $socket; # sysread / close won't work on fileno
$selectlist{$name} = $hash;
$hash->{ConnectionState} = 'connected';
WS980_Log($hash, $logLevel, "Socket Connected");
return 1;
}
#------------------------------------------------------------------------------------------------------
# ReadFn
#------------------------------------------------------------------------------------------------------
sub WS980_ReadFn($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "called");
my $rawbuf;
my $len = sysread($hash->{CD}, $rawbuf, 10240);
WS980_Log($hash, 4, "received reply: " . WS980_hexDump($rawbuf));
$hash->{helper}{requestInProgress} = 0;
if (!defined($len) or !$len or $len < 1 ) {
WS980_Close($hash);
return;
}
my ($typeStr, $buf) = WS980_handleReply($hash, $rawbuf);
$typeStr = "" if (!defined($typeStr));
if (AttrVal($name, "showRawBuffer", "0") eq "1") {
readingsSingleUpdate($hash, "rawBuffer_" . $typeStr, WS980_hexDump($rawbuf), 1);
} else {
CommandDeleteReading(undef, "$name rawBuffer.*");
}
if ($typeStr ne "" && defined($buf)) {
if ($typeStr eq "firmware") {
WS980_handleSingleValuesUpdate($hash, $typeStr, $buf);
} else {
WS980_handleMultiValuesUpdate($hash, $typeStr, $buf);
}
WS980_doPostUpdate($hash, $typeStr);
}
else
{
WS980_Log($hash, 1, "looks like the reply could not be decoded, skipping");
}
WS980_writeNextActiveRequest($hash);
}
#------------------------------------------------------------------------------------------------------
# called just after updating readings
#------------------------------------------------------------------------------------------------------
sub WS980_doPostUpdate($$)
{
my ($hash, $typeStr) = @_;
my $name = $hash->{NAME};
if ($typeStr eq "current") {
WS980_updateState($hash);
WS980_updateRain24h($hash);
WS980_updateRelPressure($hash);
WS980_updateEvents($hash);
}
}
#------------------------------------------------------------------------------------------------------
# updates the state-reading
#------------------------------------------------------------------------------------------------------
sub WS980_updateState($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $val = 'T: ' . ReadingsNum($name, "temperature", 0.0) . AttrVal($name, UNIT_CONVERSIONS->{"°C"}{"attr"}, "°C") . " "
. 'H: ' . ReadingsNum($name, "humidity", 0.0) . '% '
. 'W: ' . ReadingsNum($name, "wind", 0.0) . AttrVal($name, UNIT_CONVERSIONS->{"m/s"}{"attr"}, "m/s") . " "
. 'P: ' . ReadingsNum($name, "pressureAbs", 0.0) . AttrVal($name, UNIT_CONVERSIONS->{"hPa"}{"attr"}, "hPa") . " ";
readingsSingleUpdate($hash, "state", $val, 1);
}
#------------------------------------------------------------------------------------------------------
# calculates and updates the rain24h-reading
#------------------------------------------------------------------------------------------------------
sub WS980_updateRain24h($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $interval = 3600;
my $lastTS = ReadingsNum($name, ".rain24h_lastTS", 0);
my $curTS = int(gettimeofday() / $interval);
return if ($lastTS == $curTS);
WS980_Log($hash, 5, "updating rain24h ...");
readingsSingleUpdate($hash, ".rain24h_lastTS", $curTS, 1);
my $curRainTotal = ReadingsNum($name, "rainTotal", -1);
return if ($curRainTotal == -1);
my @values = split(/[|]/, ReadingsVal($name, ".rain24h_hourly", ""));
push(@values, $curRainTotal);
my $count = scalar(@values);
if ($count > 24) {
my $lastRainTotal = shift(@values);
readingsSingleUpdate($hash, "rain24h", sprintf("%.1f", $curRainTotal - $lastRainTotal), 1);
} else {
my $lastRainTotal = $values[0];
readingsSingleUpdate($hash, "rain24h", sprintf("(%.1f in %dh)", $curRainTotal - $lastRainTotal, $count), 1);
}
readingsSingleUpdate($hash, ".rain24h_hourly", join('|', @values), 1);
}
#------------------------------------------------------------------------------------------------------
# calculates and update the relative pressure
#------------------------------------------------------------------------------------------------------
sub WS980_updateRelPressure($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $altitude = 0;
if (defined($attr{$name}{"altitude"})){
$altitude = $attr{$name}{"altitude"};
} elsif (defined($attr{"global"}{"altitude"})) {
$altitude = $attr{"global"}{"altitude"};
}
my $relPressure = WS980_calculateRelPressure_QFF(
ReadingsNum($name, "temperature", 0.0),
ReadingsNum($name, "pressureAbs", 0.0),
$altitude,
ReadingsNum($name, "humidity", 0.0));
readingsSingleUpdate($hash, "pressureRel_calculated", sprintf("%.1f", $relPressure), 1);
}
#------------------------------------------------------------------------------------------------------
# https://www.symcon.de/forum/threads/6480-Relativen-Luftdruck-aus-absoluten-Luftdruck-errechnen
# QNH: Luftdruckangabe auf Meereshöhe nach einer Standardatmosphäre (nach ICAO) reduziert (Flughäfen,
# CWOP-Stationen, APRS) nach http://dk0te.ba-ravensburg.de/cgi-bin/navi?m=WX_BAROMETER
#------------------------------------------------------------------------------------------------------
sub WS980_calculateRelPressure_QNH($$$)
{
my ($Temperature, $AirPressureAbsolute, $Altitude) = @_;
my $g_n = 9.80665; # Erdbeschleunigung (m/s^2)
my $gam = 0.0065; # Temperaturabnahme in K pro geopotentiellen Metern (K/gpm)
my $R = 287.06; # Gaskonstante für trockene Luft (R = R_0 / M)
my $M = 0.0289644; # Molare Masse trockener Luft (J/kgK)
my $R_0 = 8.314472; # allgemeine Gaskonstante (J/molK)
my $T_0 = 273.15; # Umrechnung von °C in K
my $p = $AirPressureAbsolute * ((($gam * $Altitude + $Temperature + $T_0) / ($Temperature + $T_0)) ** ($g_n / ($R * $gam)));
return $p;
}
#------------------------------------------------------------------------------------------------------
# https://www.symcon.de/forum/threads/6480-Relativen-Luftdruck-aus-absoluten-Luftdruck-errechnen
# QFF: Luftdruckangabe auf Meereshöhe umgerechnet (DWD)
# nach http://dk0te.ba-ravensburg.de/cgi-bin/navi?m=WX_BAROMETER
#------------------------------------------------------------------------------------------------------
sub WS980_calculateRelPressure_QFF($$$$)
{
my ($Temperature, $AirPressureAbsolute, $Altitude, $Humidity) = @_;
my $g_n = 9.80665; # Erdbeschleunigung (m/s^2)
my $gam = 0.0065; # Temperaturabnahme in K pro geopotentiellen Metern (K/gpm)
my $R = 287.06; # Gaskonstante für trockene Luft (R = R_0 / M)
my $M = 0.0289644; # Molare Masse trockener Luft (J/kgK)
my $R_0 = 8.314472; # allgemeine Gaskonstante (J/molK)
my $T_0 = 273.15; # Umrechnung von °C in K
my $C = 0.11; # DWD-Beiwert für die Berücksichtigung der Luftfeuchte
my $E_0 = 6.11213; # (hPa)
my $f_rel = $Humidity / 100; # relative Luftfeuchte (0-1.0)
# momentaner Stationsdampfdruck (hPa)
my $e_d = $f_rel * $E_0 * exp((17.5043 * $Temperature) / (241.2 + $Temperature));
my $p = $AirPressureAbsolute * exp(($g_n * $Altitude) / ($R * ($Temperature + $T_0 + $C * $e_d + (($gam * $Altitude) / 2))));
return $p;
}
#------------------------------------------------------------------------------------------------------
# converts the 'event'-attribute into an internal structure
#------------------------------------------------------------------------------------------------------
sub WS980_parseEventsAttr($$)
{
my ($hash, $attrVal) = @_;
my $name = $hash->{NAME};
my %oldEventsConfig;
if ($hash->{helper}{eventsConfig}) {
%oldEventsConfig = %{$hash->{helper}{eventsConfig}}
}
# compact the input to be able to parse it right
$attrVal =~ s/\n/|/g; # newline -> |
$attrVal =~ s/\s//g; # " " -> ""
$attrVal =~ s/\|+/|/g; # || -> |
WS980_Log($hash, 5, "WS980_parseEventsAttr for $attrVal");
# parse attribute
my %eventsConfig;
my @cfgs = split("[|]", $attrVal);
foreach my $cfg (@cfgs) {
my ($event, $readingAndLimit, $hysterese) = split(/[,:]/, $cfg); # dusk:brightness<20,20
my ($srcReading, $type, $limit) = split(/\b/, $readingAndLimit); # brightness<20
my $eventReading = "is" . uc(substr($event, 0, 1)) . substr($event, 1); # isDusk
$eventsConfig{$eventReading}{"src"} = $srcReading;
$eventsConfig{$eventReading}{"type"} = $type;
$eventsConfig{$eventReading}{"limit"} = $limit;
$eventsConfig{$eventReading}{"hyst"} = int($hysterese);
WS980_Log($hash, 5, "adding event-configuration for $eventReading: $srcReading, $type, $limit, $hysterese");
}
# remember config in $hash->{helper}
$hash->{helper}{eventsConfig} = \%eventsConfig;
# delete removed events
foreach my $oldReading (keys %oldEventsConfig) {
if (!defined($eventsConfig{$oldReading})) {
WS980_Log($hash, 5, "removing event-configuration for $oldReading");
CommandDeleteReading( undef, "$name ". $oldReading);
CommandDeleteReading( undef, "$name ".".".$oldReading."_hyst");
}
}
# initialize new events if init was done already
if ($init_done) {
WS980_updateEvents($hash)
}
}
#------------------------------------------------------------------------------------------------------
# updates all events from readings
#------------------------------------------------------------------------------------------------------
sub WS980_updateEvents($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "called");
if (!$hash->{helper}{eventsConfig}) {
return
}
my %beConfig = %{$hash->{helper}{eventsConfig}};
# handle events
readingsBeginUpdate($hash);
foreach my $readingName (keys %beConfig)
{
my $hystReadingName = "." . $readingName . "_hyst";
my $src = $beConfig{$readingName}{"src"}; # brightness
my $type = $beConfig{$readingName}{"type"}; # <|>
my $limit = $beConfig{$readingName}{"limit"}; # 5000
my $hyst = $beConfig{$readingName}{"hyst"}; # 100
my $prevState = ReadingsNum($name, $readingName, -1); # 0, 1
my $prevHystState = ReadingsNum($name, $hystReadingName, -1); # 0, 1
my $srcValue = ReadingsNum($name, $src, -1); # 23540
if ($type eq "<") {
if ($prevState == -1) {
WS980_Log($hash, 5, "adding event $readingName");
readingsBulkUpdate($hash, $readingName, $srcValue <= $limit ? "1" : "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 0);
} else {
if ($srcValue <= maxNum(0, $limit - $hyst)) {
readingsBulkUpdate($hash, $readingName, "1", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 0);
} elsif ($srcValue <= $limit) {
if ($prevState == 0 && $prevHystState == 0) {
readingsBulkUpdate($hash, $readingName, "1", 1);
readingsBulkUpdate($hash, $hystReadingName, "1", 0);
}
} elsif ($srcValue <= $limit + $hyst) {
if ($prevState == 1 && $prevHystState == 0) {
readingsBulkUpdate($hash, $readingName, "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "1", 0);
}
} else {
readingsBulkUpdate($hash, $readingName, "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 0);
}
}
}
elsif ($type eq ">") {
if ($prevState == -1) {
WS980_Log($hash, 5, "adding event $readingName");
readingsBulkUpdate($hash, $readingName, $srcValue >= $limit ? "1" : "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 0);
} else {
if ($srcValue >= $limit + $hyst) {
readingsBulkUpdate($hash, $readingName, "1", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 01);
} elsif ($srcValue >= $limit) {
if ($prevState == 0 && $prevHystState == 0) {
readingsBulkUpdate($hash, $readingName, "1", 1);
readingsBulkUpdate($hash, $hystReadingName, "1", 0);
}
} elsif ($srcValue >= maxNum(0, $limit - $hyst)) {
if ($prevState == 1 && $prevHystState == 0) {
readingsBulkUpdate($hash, $readingName, "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "1", 0);
}
} else {
readingsBulkUpdate($hash, $readingName, "0", 1);
readingsBulkUpdate($hash, $hystReadingName, "0", 0);
}
}
}
else {
# ERROR
}
}
readingsEndUpdate($hash, 1);
}
#------------------------------------------------------------------------------------------------------
# WriteFn
#------------------------------------------------------------------------------------------------------
sub WS980_WriteFn($$)
{
my ($hash, $buf) = @_;
my $name = $hash->{NAME};
WS980_Log($hash, 5, "called");
if (!$hash->{CD}) {
WS980_Log($hash, 1, "socket not connected");
return;
}
WS980_Log($hash, 5, "sending " . WS980_hexDump($buf));
my $bytes = syswrite($hash->{CD}, $buf);
# success?
if (defined($bytes) && $bytes == length($buf)) {
$hash->{helper}{requestInProgress} = 1;
WS980_Log($hash, 5, "sent $bytes bytes");
} else {
my $err = "Wrote incomplete data";
if (!defined ($bytes)) {
$err = $!;
}
WS980_error($hash, "error sending data: " . $err);
WS980_Close($hash);
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# close
#------------------------------------------------------------------------------------------------------
sub WS980_Close($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
return if( !$hash->{CD} );
close($hash->{CD}) if($hash->{CD});
delete($hash->{FD});
delete($hash->{CD});
delete($selectlist{$name});
$hash->{ConnectionState} = 'disconnected';
my $logLevel = AttrVal($name, "silentReconnect", "") eq "1" ? 4 : 3;
WS980_Log($hash, $logLevel, "Socket Disconnected");
}
#------------------------------------------------------------------------------------------------------
# used to automatically extract attributes from UNIT_CONVERSIONS for AttrList
#------------------------------------------------------------------------------------------------------
sub WS980_extractAttrsFromUnits()
{
my $retval;
foreach my $unit (keys %{UNIT_CONVERSIONS()}) {
my $attr = UNIT_CONVERSIONS->{$unit}{"attr"} . ":";
my @units = keys %{UNIT_CONVERSIONS->{$unit}{"fnc"}};
$retval .= $attr . join(",", @units) . " ";
}
return $retval;
}
#------------------------------------------------------------------------------------------------------
# converts a binary input to hex-string '\x23\xff' -> "23ff"
#------------------------------------------------------------------------------------------------------
sub WS980_binToHex($)
{
my ($bin) = @_;
my @array = split('', $bin);
my $hex = "";
foreach (@array) {
$hex .= sprintf("%02x", ord($_));
}
return $hex;
}
#------------------------------------------------------------------------------------------------------
# returns a readable representation of the already hex formated input like "ffff0b005004..."
#------------------------------------------------------------------------------------------------------
sub WS980_hexDump($)
{
my ($buf) = @_;
my @retval;
my @array = unpack("C*", $buf);
for (my $i = 0; $i < scalar(@array); $i++) {
push (@retval, sprintf('%02x', $array[$i]));
}
return "[" . join(" ", @retval) . "]";
}
#------------------------------------------------------------------------------------------------------
# calculates the checksum of $buf and returns it
# The checksum is the sum of each byte & 0xff
#------------------------------------------------------------------------------------------------------
sub WS980_calculateChecksum($)
{
my ($buf) = @_;
return unpack('%16C*', $buf);
}
#------------------------------------------------------------------------------------------------------
# calculates the checksum of $buf[0..-2] and compares it to $buf[-1..-0]
#------------------------------------------------------------------------------------------------------
sub WS980_checkChecksum($)
{
my ($buf) = @_;
my $actual = WS980_calculateChecksum(substr($buf, -1)); # exclude the checksum
my $expected = ord(substr($buf, -1));
return $actual == $expected;
}
#------------------------------------------------------------------------------------------------------
# updates lastError-Reading and logs the message
#------------------------------------------------------------------------------------------------------
sub WS980_error($$)
{
my ($hash, $msg) = @_;
my $name = $hash->{NAME};
readingsSingleUpdate($hash, "lastError", $msg, 1);
WS980_Log($hash, 1, "ERROR: $msg");
}
#------------------------------------------------------------------------------------------------------
# Util: Log
#------------------------------------------------------------------------------------------------------
sub WS980_Log($$$)
{
my ($hash, $logLevel, $logMessage) = @_;
my $line = ( caller(0) )[2];
my $modAndSub = ( caller(1) )[3];
my $subroutine = ( split(':', $modAndSub) )[2];
my $name = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "WS980";
Log3($hash, $logLevel, "${name} (WS980::${subroutine}:${line}) " . $logMessage);
}
1;
=pod
=item device
=item summary Module to request weather data form WS980WiFi weather stations
=item summary_DE Modul zum Abfragen von Wetterdaten aus WS980WiFi-Wetterstationen
=begin html
<a id="WS980"></a>
<h3>WS980</h3>
<ul>
<b>WS980 - Requests weather data locally from WS980WiFi weather stations</b><br>
<br>
<a id="WS980-define"></a>
<b>Define</b><br>
<br>
<code>define &lt;name&gt; WS980 [IP] [INTERVAL]</code><br>
<br>
<code>[IP]</code> Optional: The IP of the WS980WiFi. If no IP is given, the station is auto-discovered.<br>
<code>[INTERVAL]</code> Optional: The interval in seconds to request updates, default: 30 seconds.<br>
<br>
Example:<br>
<br>
<code>define ws980wifi WS980 192.168.2.177 60</code><br>
<br>
This statement creates an WS980 instance with the name ws980wifi with the IP 192.168.2.177.<br>
With an interval of 60 seconds the weather information are requested.
<br>
<br>
<!-- <a name="WS980readings"></a> -->
<b>Readings</b>
<ul>
<li>
<dt><code><b>temperatureInside</b> [<b>°C</b>]</code></dt>
The IN-temperature at the display.
</li>
<li>
<dt><code><b>temperature</b> [<b>°C</b>]</code></dt>
The temperature measured by the outdoor probe.
</li>
<li>
<dt><code><b>dewPoint</b> [<b>°C</b>]</code></dt>
The dew-point calculated by the weather station.
</li>
<li>
<dt><code><b>windChill</b> [<b>°C</b>]</code></dt>
The wind-chill calculated by the weather station.
</li>
<li>
<dt><code><b>heatIndex</b> [<b>°C</b>]</code></dt>
The heat-index calculated by the weather station.
</li>
<li>
<dt><code><b>humidityInside</b> [<b>%</b>]</code></dt>
The IN-humidity at the display.
</li>
<li>
<dt><code><b>humidity</b> [<b>%</b>]</code></dt>
The humidity measured by the outdoor probe.
</li>
<li>
<dt><code><b>pressureAbs</b> [<b>hPa</b>]</code></dt>
The absolute pressure measured by the outdoor probe.
</li>
<li>
<dt><code><b>pressureRel</b> [<b>hPa</b>]</code></dt>
The relative pressure measured by the outdoor probe. This only differs from <i>pressureAbs</i> if you have an offset configured in your weather station.
</li>
<li>
<dt><code><b>pressureRel_calculated</b> [<b>hPa</b>]</code></dt>
The relative pressure calculated using the QFF forumula based on temperature, pressureAbs, altitude and humidity.
</li>
<li>
<dt><code><b>windDirection</b> [<b>°</b>]</code></dt>
The wind-direction measured by the outdoor probe.
</li>
<li>
<dt><code><b>wind</b> [<b>m/s</b>]</code></dt>
The wind-seed measured by the outdoor probe.
</li>
<li>
<dt><code><b>windGusts</b> [<b>m/s</b>]</code></dt>
The speed of wind-gusts measured by the outdoor probe.
</li>
<!--
<li>
<dt><code><b>rainEvent</b> [<b>mm</b>]</code></dt>
**Not Supported**.
</li>
-->
<li>
<dt><code><b>rainRate</b> [<b>mm</b>]</code></dt>
The current rain-rate measured by the outdoor probe.
</li>
<!--
<li>
<dt><code><b>rainPerHour</b> [<b>mm</b>]</code></dt>
**Not Supported** The rain-rate per hour measured by the outdoor probe.
</li>
-->
<li>
<dt><code><b>rainPerDay</b> [<b>mm</b>]</code></dt>
The rain-rate per day measured by the outdoor probe.
</li>
<li>
<dt><code><b>rainPerWeek</b> [<b>mm</b>]</code></dt>
The rain-rate per week measured by the outdoor probe.
</li>
<li>
<dt><code><b>rainPerMonth</b> [<b>mm</b>]</code></dt>
The rain-rate per month measured by the outdoor probe.
</li>
<li>
<dt><code><b>rainPerYear</b> [<b>mm</b>]</code></dt>
The rain-rate per year measured by the outdoor probe.
</li>
<li>
<dt><code><b>rainTotal</b> [<b>mm</b>]</code></dt>
The total rain-rate measured by the outdoor probe.
</li>
<li>
<dt><code><b>rain24h</b> [<b>mm</b>]</code></dt>
The amount of rain in the last 24h. This value is calculated from <code>rainTotal</code> and is updated once per hour. In the first 24h, the amunt is displayed in "()" and shows the count of hours recorded already.
</li>
<li>
<dt><code><b>brightness</b> [<b>lux</b>]</code></dt>
The brightness measured by the outdoor probe.
</li>
<li>
<dt><code><b>uv</b> [<b>uW/m²</b>]</code></dt>
The raw UV-values measured by the outdoor probe.
</li>
<li>
<dt><code><b>uvIndex</b> [<b>0-15</b>]</code></dt>
The UV-Index calculated by the weather station.
</li>
</ul>
<br>
<a id="WS980-set"></a>
<b>Set</b>
<ul>
<li><a id="WS980-set-update"></a>
<i>update</i><br>
manually update current weather data
</li>
</ul>
<br>
<a id="WS980-attr"></a>
<b>Attributes</b>
<ul>
<li><a id="WS980-attr-altitude"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>altitude </b>&lt;<b>height</b>&gt;</code></dt>
Specifies the mean sea level in meters. Default is 0. Used to calculate the <code>pressureRel_calculated</code>-reading. If unset, the altitude from global is used.
</li>
<li><a id="WS980-attr-connection"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>connection </b>&lt;<b>Keep-Alive</b>|<b>Close</b>&gt;</code></dt>
<code>Keep-Alive</code>: The connection to the WS980 is kept open as long as possible. Reconnect is only done if necessary. <code>Keep-Alive</code> is default and a good setting in most cases.<br>
<code>Close</code>: The connection is opened on-the-fly and closed directly after doing requests. <code>Close</code> should only be used if you have multiple clients connection to your WS980 which might cause frequent read-timeouts. <code>ConnectionState</code> will display <code>disconnected</code> most of the time, this is OK!
</li>
<li><a id="WS980-attr-invalidValues"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>invalidValues </b>&lt;<b>updateReading</b>|<b>skip</b>|<b>skipAndLog</b>&gt;</code></dt>
<code>updateReading</code>: The reading will be set to n/a when an invalid value is received. <code>updateReading</code> is default.<br>
<code>skip</code>: Invalid values will silently be skipped, the corresponding reading will not be updated.<br>
<code>skipAndLog</code>: Invalid values will be logged, and the corresponding reading will not be updated.<br>
</li>
<li><a id="WS980-attr-events"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>events </b>&lt;<b>Configuration</b>&gt;|&lt;<b>Configuration</b>&gt;|...</code></dt>
Allows to configure custom events based on the readings of this instance.<br>
&lt;<b>Configuration</b>&gt; must have the form:
<b><code>NAME:READING&lt;LIMIT,HYSTERESIS</code></b> or <b><code>NAME:READING&gt;LIMIT,HYSTERESIS</code></b>.
<ul>
<li><b><code>NAME</code></b>: The name used for the reading and the event. To reduce the possibility of collisions with existing readings, the Name of the reading will always be prefixed with 'is'.</li>
<li><b><code>READING</code></b>:The name of the reading which will be compared to LIMIT to set the resulting event to 0 or 1.</li>
<li><b><code>&lt; or &gt</code></b>: For '&lt;' the reading-value must be less than the LIMIT to result in 1, for '&gt;' the reading-value must be above LIMIT to result in 1.</li>
<li><b><code>LIMIT</code></b>: The value which will be compared to the READINGs current value.</li>
<li><b><code>HYSTERESIS</code></b>: The hysteresis-value is used to reduce fast flipping of 0 and 1.</li>
</ul>
Configurations can be separated by either "|" or newline. All whitespace will be removed when parsing <i>events</i>.
<br>
<b>Examples:</b><br>
<dt><code><b>attr</b> &lt;name&gt; <b>events dusk:brightness&lt;30,20</b></code></dt>
A new reading <i>isDusk</i> will be autogenerated with possible values 0 or 1. As soon as the <i>brightness</i> drops below 30lux, <i>isDusk</i> will switch to 1. The hystereses value of 20 means, that <i>isDusk</i> will switch back to 0 only after <b>either</b> the <i>brightness</i> has reached 50lux (30+20) <b>or</b> the <i>brightness</i> dropped below 10lux (30-20) and then rises above 30lux again.
<dt><code><b>attr</b> &lt;name&gt; <b>brightSunlight:brightness&gt;80000,5000</b></code></dt>
A new reading <i>isBrightSunlight</i> will be autogenerated with possible values 0 or 1. As soon as the <i>brightness</i> rises above 80000lux, <i>isBrightSunlight</i> will switch to 1. The hystereses value of 5000 means, that <i>isBrightSunlight</i> will switch back to 0 only after <b>either</b> the <i>brightness</i> has dropped below 75000lux <b>or</b> the <i>brightness</i> was above 85000lux and then drops below 80000lux again.<br>
You can concatenate as many <b>Configurations</b> with '|' as you like. For example:
<dt><code><b>attr</b> &lt;name&gt; <b>dusk:brightness&lt;30,20|brightSunlight:brightness&gt;80000,5000</b></code></dt>
<br>
</li>
<li><a id="WS980-attr-requests"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>requests </b>current todayMax ... </code></dt>
A comma or space separated list of values to requests. If empty, all known values are requested.<br>
Valid values: firmware, current, todayMax, todayMin, historyMax, historyMin.
</li>
<li><a id="WS980-attr-showRawBuffer"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>showRawBuffer </b>1</code></dt>
used for development: show raw data received from the WS980WiFi
</li>
<li><a id="WS980-attr-silentReconnect"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>silentReconnect </b>1</code></dt>
If set to 1, then it will set the loglevel for connect- and reconnect-messages to 2 instead of 1
</li>
<li><a id="WS980-attr-unit_temperature"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>unit_temperature </b>&lt;<b>unit</b>&gt;</code></dt>
set the unit used for temperature-readings. Default: °C
</li>
<li><a id="WS980-attr-unit_pressure"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>unit_pressure </b>&lt;<b>unit</b>&gt;</code></dt>
set the unit used for pressure-readings. Default: hPa
</li>
<li><a id="WS980-attr-unit_wind"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>unit_wind </b>&lt;<b>unit</b>&gt;</code></dt>
set the unit used for wind-readings. Default: m/s
</li>
<li><a id="WS980-attr-unit_rain"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>unit_rain </b>&lt;<b>unit</b>&gt;</code></dt>
set the unit used for rain-readings. Default: mm
</li>
<li><a id="WS980-attr-unit_light"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>unit_light </b>&lt;<b>unit</b>&gt;</code></dt>
set the unit used for brightness-readings. Default: lux
</li>
<li><a id="WS980-attr-disable"></a>
<dt><code><b>attr</b> &lt;name&gt; <b>disable </b>1</code></dt>
disables this WS980-instance
</li>
</ul>
</ul>
=end html
# =begin html_DE
#
#
# =end html_DE
# Ende der Commandref
=cut