mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-02-25 09:55:38 +00:00
1630 lines
55 KiB
Perl
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 <name> 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> <name> <b>altitude </b><<b>height</b>></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> <name> <b>connection </b><<b>Keep-Alive</b>|<b>Close</b>></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> <name> <b>invalidValues </b><<b>updateReading</b>|<b>skip</b>|<b>skipAndLog</b>></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> <name> <b>events </b><<b>Configuration</b>>|<<b>Configuration</b>>|...</code></dt>
|
|
Allows to configure custom events based on the readings of this instance.<br>
|
|
<<b>Configuration</b>> must have the form:
|
|
<b><code>NAME:READING<LIMIT,HYSTERESIS</code></b> or <b><code>NAME:READING>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>< or ></code></b>: For '<' the reading-value must be less than the LIMIT to result in 1, for '>' 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> <name> <b>events dusk:brightness<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> <name> <b>brightSunlight:brightness>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> <name> <b>dusk:brightness<30,20|brightSunlight:brightness>80000,5000</b></code></dt>
|
|
<br>
|
|
</li>
|
|
<li><a id="WS980-attr-requests"></a>
|
|
<dt><code><b>attr</b> <name> <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> <name> <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> <name> <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> <name> <b>unit_temperature </b><<b>unit</b>></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> <name> <b>unit_pressure </b><<b>unit</b>></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> <name> <b>unit_wind </b><<b>unit</b>></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> <name> <b>unit_rain </b><<b>unit</b>></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> <name> <b>unit_light </b><<b>unit</b>></code></dt>
|
|
set the unit used for brightness-readings. Default: lux
|
|
</li>
|
|
<li><a id="WS980-attr-disable"></a>
|
|
<dt><code><b>attr</b> <name> <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
|