fhem-mirror/FHEM/00_MYSENSORS.pm
2021-09-22 20:29:54 +00:00

1106 lines
33 KiB
Perl

##############################################
#
# fhem driver for MySensors serial or network gateway (see http://mysensors.org)
#
# Copyright (C) 2014 Norbert Truchsess
# Copyright (C) 2019 Hauswart@forum.fhem.de
# Copyright (C) 2019 Beta-User@forum.fhem.de
#
# This file is part of fhem.
#
# Fhem is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Fhem is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
#
# $Id$
#
##############################################
package MYSENSORS; ## no critic 'Package declaration'
## no critic 'constant'
use strict;
use warnings;
use List::Util qw(first);
use Exporter ('import');
use DevIo;
use GPUtils qw(:all);
use Scalar::Util qw(looks_like_number);
use Time::HiRes qw(gettimeofday);
sub main::MYSENSORS_Initialize { goto &Initialize };
my %sets = (
connect => [qw(noArg)],
disconnect => [qw(noArg)],
'inclusion-mode' => [qw(on off)],
);
my %gets = (
version => ''
);
my @clients = qw(
MYSENSORS_DEVICE
);
sub Initialize {
my $hash = shift // return;
# Provider
$hash->{Clients} = join q{:}, @clients;
$hash->{ReadyFn} = \&Ready;
$hash->{ReadFn} = \&Read;
# Consumer
$hash->{DefFn} = \&Define;
$hash->{UndefFn} = \&Undef;
$hash->{SetFn} = \&Set;
$hash->{AttrFn} = \&Attr;
my @attrList = (
'disable:0,1',
'first-sensorid:selectnumbers,1,1,255,0,lin',
'last-sensorid:selectnumbers,2,1,255,0,lin',
qw(
autocreate:1
requestAck:1
stateFormat
OTA_firmwareConfig
)
);
$hash->{AttrList} = $hash->{AttrList} = join q{ }, @attrList;
return;
}
BEGIN { GP_Import(
qw(
init_done
defs
CommandDefine
CommandModify
CommandAttr
readingsSingleUpdate
DevIo_OpenDev
DevIo_SimpleWrite
DevIo_SimpleRead
DevIo_getState
DevIo_CloseDev
RemoveInternalTimer
InternalTimer
AttrVal
Log3
FileRead
IsDisabled
)
)
};
my %sensorAttr = (
LIGHT => ['setCommands on:V_LIGHT:1 off:V_LIGHT:0' ],
ARDUINO_NODE => [ 'config M' ],
ARDUINO_REPEATER_NODE => [ 'config M' ],
);
sub Define {
my $hash = shift // return;
InternalTimer(time, \&Start, $hash,0);
return;
}
sub Undef {
Stop(shift);
return;
}
#-- Message types
use constant {
C_PRESENTATION => 0,
C_SET => 1,
C_REQ => 2,
C_INTERNAL => 3,
C_STREAM => 4,
};
use constant commands => qw( C_PRESENTATION C_SET C_REQ C_INTERNAL C_STREAM );
sub commandToStr {
return (commands)[shift];
}
#-- Variable types
use constant {
V_TEMP => 0,
V_HUM => 1,
V_STATUS => 2,
V_PERCENTAGE => 3,
V_PRESSURE => 4,
V_FORECAST => 5,
V_RAIN => 6,
V_RAINRATE => 7,
V_WIND => 8,
V_GUST => 9,
V_DIRECTION => 10,
V_UV => 11,
V_WEIGHT => 12,
V_DISTANCE => 13,
V_IMPEDANCE => 14,
V_ARMED => 15,
V_TRIPPED => 16,
V_WATT => 17,
V_KWH => 18,
V_SCENE_ON => 19,
V_SCENE_OFF => 20,
V_HVAC_FLOW_STATE => 21,
V_HVAC_SPEED => 22,
V_LIGHT_LEVEL => 23,
V_VAR1 => 24,
V_VAR2 => 25,
V_VAR3 => 26,
V_VAR4 => 27,
V_VAR5 => 28,
V_UP => 29,
V_DOWN => 30,
V_STOP => 31,
V_IR_SEND => 32,
V_IR_RECEIVE => 33,
V_FLOW => 34,
V_VOLUME => 35,
V_LOCK_STATUS => 36,
V_LEVEL => 37,
V_VOLTAGE => 38,
V_CURRENT => 39,
V_RGB => 40,
V_RGBW => 41,
V_ID => 42,
V_UNIT_PREFIX => 43,
V_HVAC_SETPOINT_COOL => 44,
V_HVAC_SETPOINT_HEAT => 45,
V_HVAC_FLOW_MODE => 46,
V_TEXT => 47,
V_CUSTOM => 48,
V_POSITION => 49,
V_IR_RECORD => 50,
V_PH => 51,
V_ORP => 52,
V_EC => 53,
V_VAR => 54,
V_VA => 55,
V_POWER_FACTOR => 56,
};
use constant variableTypes => qw(
V_TEMP V_HUM V_STATUS V_PERCENTAGE V_PRESSURE V_FORECAST V_RAIN
V_RAINRATE V_WIND V_GUST V_DIRECTION V_UV V_WEIGHT V_DISTANCE
V_IMPEDANCE V_ARMED V_TRIPPED V_WATT V_KWH V_SCENE_ON V_SCENE_OFF
V_HVAC_FLOW_STATE V_HVAC_SPEED V_LIGHT_LEVEL
V_VAR1 V_VAR2 V_VAR3 V_VAR4 V_VAR5
V_UP V_DOWN V_STOP V_IR_SEND V_IR_RECEIVE V_FLOW V_VOLUME V_LOCK_STATUS
V_LEVEL V_VOLTAGE V_CURRENT V_RGB V_RGBW V_ID V_UNIT_PREFIX
V_HVAC_SETPOINT_COOL V_HVAC_SETPOINT_HEAT V_HVAC_FLOW_MODE
V_TEXT V_CUSTOM V_POSITION V_IR_RECORD V_PH V_ORP V_EC
V_VAR V_VA V_POWER_FACTOR
);
sub variableTypeToStr {
return (variableTypes)[shift];
}
sub variableTypeToIdx {
my $var = shift // return;
return first { (variableTypes)[$_] eq $var } 0 .. scalar(variableTypes);
}
#-- Internal messages
use constant {
I_BATTERY_LEVEL => 0,
I_TIME => 1,
I_VERSION => 2,
I_ID_REQUEST => 3,
I_ID_RESPONSE => 4,
I_INCLUSION_MODE => 5,
I_CONFIG => 6,
I_FIND_PARENT => 7,
I_FIND_PARENT_RESPONSE => 8,
I_LOG_MESSAGE => 9,
I_CHILDREN => 10,
I_SKETCH_NAME => 11,
I_SKETCH_VERSION => 12,
I_REBOOT => 13,
I_GATEWAY_READY => 14,
I_REQUEST_SIGNING => 15,
I_GET_NONCE => 16,
I_GET_NONCE_RESPONSE => 17,
I_HEARTBEAT_REQUEST => 18,
I_PRESENTATION => 19,
I_DISCOVER_REQUEST => 20,
I_DISCOVER_RESPONSE => 21,
I_HEARTBEAT_RESPONSE => 22,
I_LOCKED => 23, # Node is locked (reason in string-payload)
I_PING => 24, # Ping sent to node, payload incremental hop counter
I_PONG => 25, # In return to ping, sent back to sender, payload incremental hop counter
I_REGISTRATION_REQUEST => 26, # Register request to GW
I_REGISTRATION_RESPONSE => 27, # Register response from GW
I_DEBUG => 28,
I_SIGNAL_REPORT_REQUEST => 29,
I_SIGNAL_REPORT_REVERSE => 30,
I_SIGNAL_REPORT_RESPONSE => 31,
I_PRE_SLEEP_NOTIFICATION => 32,
I_POST_SLEEP_NOTIFICATION => 33,
};
use constant internalMessageTypes => qw{
I_BATTERY_LEVEL I_TIME I_VERSION I_ID_REQUEST I_ID_RESPONSE
I_INCLUSION_MODE I_CONFIG I_FIND_PARENT I_FIND_PARENT_RESPONSE
I_LOG_MESSAGE I_CHILDREN I_SKETCH_NAME I_SKETCH_VERSION
I_REBOOT I_GATEWAY_READY I_REQUEST_SIGNING I_GET_NONCE I_GET_NONCE_RESPONSE
I_HEARTBEAT_REQUEST I_PRESENTATION I_DISCOVER_REQUEST
I_DISCOVER_RESPONSE I_HEARTBEAT_RESPONSE I_LOCKED I_PING I_PONG
I_REGISTRATION_REQUEST I_REGISTRATION_RESPONSE I_DEBUG
I_SIGNAL_REPORT_REQUEST I_SIGNAL_REPORT_REVERSE I_SIGNAL_REPORT_RESPONSE
I_PRE_SLEEP_NOTIFICATION I_POST_SLEEP_NOTIFICATION };
sub internalMessageTypeToStr {
return (internalMessageTypes)[shift];
}
#-- Sensor types
use constant {
S_DOOR => 0,
S_MOTION => 1,
S_SMOKE => 2,
S_BINARY => 3,
S_DIMMER => 4,
S_COVER => 5,
S_TEMP => 6,
S_HUM => 7,
S_BARO => 8,
S_WIND => 9,
S_RAIN => 10,
S_UV => 11,
S_WEIGHT => 12,
S_POWER => 13,
S_HEATER => 14,
S_DISTANCE => 15,
S_LIGHT_LEVEL => 16,
S_ARDUINO_NODE => 17,
S_ARDUINO_REPEATER_NODE => 18,
S_LOCK => 19,
S_IR => 20,
S_WATER => 21,
S_AIR_QUALITY => 22,
S_CUSTOM => 23,
S_DUST => 24,
S_SCENE_CONTROLLER => 25,
S_RGB_LIGHT => 26,
S_RGBW_LIGHT => 27,
S_COLOR_SENSOR => 28,
S_HVAC => 29,
S_MULTIMETER => 30,
S_SPRINKLER => 31,
S_WATER_LEAK => 32,
S_SOUND => 33,
S_VIBRATION => 34,
S_MOISTURE => 35,
S_INFO => 36,
S_GAS => 37,
S_GPS => 38,
S_WATER_QUALITY => 39,
};
use constant sensorTypes => qw{
S_DOOR S_MOTION S_SMOKE S_BINARY S_DIMMER S_COVER S_TEMP S_HUM S_BARO S_WIND
S_RAIN S_UV S_WEIGHT S_POWER S_HEATER S_DISTANCE S_LIGHT_LEVEL
S_ARDUINO_NODE S_ARDUINO_REPEATER_NODE S_LOCK S_IR S_WATER S_AIR_QUALITY
S_CUSTOM S_DUST S_SCENE_CONTROLLER S_RGB_LIGHT S_RGBW_LIGHT S_COLOR_SENSOR
S_HVAC S_MULTIMETER S_SPRINKLER S_WATER_LEAK S_SOUND S_VIBRATION
S_MOISTURE S_INFO S_GAS S_GPS S_WATER_QUALITY
};
sub sensorTypeToStr {
return (sensorTypes)[shift];
}
sub sensorTypeToIdx {
my $var = shift // return;
return first { (sensorTypes)[$_] eq $var } 0 .. scalar(sensorTypes);
}
#-- Datastream types
use constant {
ST_FIRMWARE_CONFIG_REQUEST => 0,
ST_FIRMWARE_CONFIG_RESPONSE => 1,
ST_FIRMWARE_REQUEST => 2,
ST_FIRMWARE_RESPONSE => 3,
ST_SOUND => 4,
ST_IMAGE => 5,
};
use constant datastreamTypes => qw{
ST_FIRMWARE_CONFIG_REQUEST ST_FIRMWARE_CONFIG_RESPONSE
ST_FIRMWARE_REQUEST ST_FIRMWARE_RESPONSE ST_SOUND ST_IMAGE
};
sub datastreamTypeToStr {
return (datastreamTypes)[shift];
}
#-- Payload types
use constant {
P_STRING => 0,
P_BYTE => 1,
P_INT16 => 2,
P_UINT16 => 3,
P_LONG32 => 4,
P_ULONG32 => 5,
P_CUSTOM => 6,
P_FLOAT32 => 7,
};
use constant payloadTypes => qw{ P_STRING P_BYTE P_INT16 P_UINT16 P_LONG32 P_ULONG32 P_CUSTOM P_FLOAT32 };
sub payloadTypeToStr {
return (payloadTypes)[shift];
}
sub subTypeToStr {
my $cmd = shift;
my $subType = shift // return;
# Convert subtype to string, depending on message type
if ($cmd == C_SET) {
return $subType = (variableTypes)[$subType];
}
if ($cmd == C_INTERNAL) {
return $subType = (internalMessageTypes)[$subType];
}
if ($cmd == C_STREAM) {
return $subType = (datastreamTypes)[$subType];
}
if ($cmd == C_PRESENTATION) {
return $subType = (sensorTypes)[$subType];
}
if ($cmd == C_REQ) {
return $subType = (variableTypes)[$subType];
}
return $subType = "<UNKNOWN_$subType>";
}
sub Set {
my $hash = shift;
my $name = shift;
my $command = shift // return "set $name needs at least one argument !";
my $value = shift // '';
return "Unknown argument $command, choose one of "
. join(" ", map {
@{$sets{$_}} ? $_
.':'
.join ',', @{$sets{$_}} : $_} sort keys %sets)
if !defined($sets{$command});
if ($command eq 'connect') {
return Start($hash);
}
if ($command eq 'disconnect') {
return Stop($hash);
}
if ($command eq 'inclusion-mode') {
sendMessage($hash,
radioId => 0,
childId => 0,
cmd => C_INTERNAL,
ack => 0,
subType => I_INCLUSION_MODE,
payload => $value eq 'on' ? 1 : 0
);
$hash->{'inclusion-mode'} = $value eq 'on' ? 1 : 0;
return;
}
return;
}
sub Attr {
my $command = shift;
my $name = shift;
my $attribute = shift;
my $value = shift;
my $hash = $defs{$name};
if ($attribute eq 'autocreate' && $init_done) {
my $mode = $command eq "set" ? 1 : 0;
$hash->{'inclusion-mode'} = $mode;
return sendMessage($hash,
radioId => $hash->{radioId},
childId => $hash->{childId},
ack => 0,
subType => I_INCLUSION_MODE,
payload => $mode
);
}
if ($attribute eq 'requestAck') {
if ($command eq 'set') {
$hash->{ack} = 1;
} else {
$hash->{ack} = 0;
$hash->{messages} = {};
$hash->{outstandingAck} = 0;
}
return;
}
if ($attribute eq 'OTA_firmwareConfig') {
if ($command eq 'set') {
return getFirmwareTypes($hash, 'set', $value);
} else {
delete $hash->{'.fwList'};
return;
}
}
if ($attribute eq 'disable') {
return Stop($hash) if $command eq 'set' && $value;
InternalTimer(time, \&Start, $hash,0);
return;
}
return;
}
sub Start {
my $hash = shift // return;
my ($dev) = split m{\s+}xms, $hash->{DEF};
$hash->{DeviceName} = $dev;
if (!AttrVal($hash->{NAME},'stateFormat',0)) {
CommandAttr(undef, "$hash->{NAME} stateFormat connection")
}
getFirmwareTypes($hash, 'start');
return if IsDisabled( $hash->{NAME} );
DevIo_CloseDev($hash);
if($dev eq 'none') {
Log3($hash, 1, "$hash->{NAME} device is none, commands will be echoed only");
$::attr{$hash->{NAME}}{disable} = 1;
return;
}
return DevIo_OpenDev($hash, 0, \&Init);
}
sub Stop {
my $hash = shift // return;
DevIo_CloseDev($hash);
RemoveInternalTimer($hash);
readingsSingleUpdate($hash,'connection','disconnected',1);
return;
}
sub Ready {
my $hash = shift // return;
return if AttrVal($hash->{NAME}, 'disable', 0);
return DevIo_OpenDev($hash, 1, \&Init) if DevIo_getState($hash) eq 'disconnected';
if ( defined $hash->{USBDev} ) {
my $po = $hash->{USBDev};
my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status;
return ( $InBytes > 0 );
}
return;
}
sub Init {
my $hash = shift // return;
my $name = $hash->{NAME};
$hash->{'inclusion-mode'} = AttrVal($name,'autocreate',0);
$hash->{ack} = AttrVal($name,'requestAck',0);
$hash->{outstandingAck} = 0;
if ($hash->{ack}) {
GP_ForallClients($hash,sub {
my $client = shift;
$hash->{messagesForRadioId}->{$client->{radioId}} = {
lastseen => -1,
nexttry => -1,
numtries => 1,
messages => [],
};
});
}
readingsSingleUpdate($hash,'connection','connected',1);
return sendMessage($hash,
radioId => 0,
childId => 0,
cmd => C_INTERNAL,
ack => 0,
subType => I_VERSION,
payload => ''
);
}
# GetConnectStatus
sub GetConnectStatus {
my $hash = shift // return;
my $name = $hash->{NAME};
Log3( $name, 4, "MySensors: GetConnectStatus called ..." );
# neuen Timer starten in einem konfigurierten Interval.
InternalTimer(gettimeofday()+300, \&GetConnectStatus, $hash);# Restart check in 5 mins again
InternalTimer(gettimeofday()+5, \&Start, $hash); #Start timer for reset if after 5 seconds RESPONSE is not received
#query heartbeat from gateway
return sendMessage($hash,
radioId => 0,
childId => 0,
cmd => C_INTERNAL,
ack => 0,
subType => I_HEARTBEAT_REQUEST,
payload => ''
)
}
sub Timer {
my $hash = shift // return;
my $now = time;
for my $radioid (keys %{$hash->{messagesForRadioId}}) {
my $msgsForId = $hash->{messagesForRadioId}->{$radioid};
if ($now > $msgsForId->{nexttry}) {
for my $msg (@{$msgsForId->{messages}}) {
my $txt = createMsg(%$msg);
Log3 ($hash->{NAME},5,"MYSENSORS outstanding ack, re-send: ".dumpMsg($msg));
DevIo_SimpleWrite($hash,"$txt\n",undef);
}
$msgsForId->{numtries}++;
$msgsForId->{nexttry} = gettimeofday()+$msgsForId->{numtries};
}
}
return _scheduleTimer($hash);
}
sub Read {
my $hash = shift // return;
my $name = $hash->{NAME};
my $buf = DevIo_SimpleRead($hash);
return "" if(!defined($buf));
my $data = $hash->{PARTIAL};
Log3 ($name, 4, "MYSENSORS/RAW: $data/$buf");
$data .= $buf;
while ($data =~ m{\n}xms) {
my $txt;
($txt,$data) = split("\n", $data, 2);
$txt =~ s/\r//xms;
if (my $msg = parseMsg($txt)) {
Log3 ($name,4,"MYSENSORS Read: ".dumpMsg($msg));
if ($msg->{ack}) {
onAcknowledge($hash,$msg);
}
RemoveInternalTimer($hash,\&GetConnectStatus);
InternalTimer(gettimeofday()+300, \&GetConnectStatus, $hash);# Restart check in 5 mins again
my $type = $msg->{cmd};
my $dispatch = {
C_PRESENTATION() => \&onPresentationMsg, #() due to constant type of key
C_SET() => \&onSetMsg,
C_REQ() => \&onRequestMsg,
C_INTERNAL() => \&onInternalMsg,
C_STREAM() => \&onStreamMsg,
};
ref $dispatch->{$type} eq 'CODE' ? $dispatch->{$type}->($hash, $msg) : Log3($hash->{NAME},2,"MYSENSORS: Dispatch failure, no valid type: >$type<");
} else {
Log3 ($name,5,"MYSENSORS Read: ".$txt."is no parsable mysensors message");
}
}
$hash->{PARTIAL} = $data;
return;
}
sub onPresentationMsg {
my ($hash,$msg) = @_;
my $client = matchClient($hash,$msg);
my $clientname;
my $sensorType = $msg->{subType};
unless ($client) {
if ($hash->{'inclusion-mode'}) {
$clientname = "MYSENSOR_$msg->{radioId}";
$clientname = "$hash->{NAME}_DEVICE_0"if defined $defs{$clientname};
CommandDefine(undef,"$clientname MYSENSORS_DEVICE $msg->{radioId}");
CommandAttr(undef,"$clientname IODev $hash->{NAME}");
CommandAttr(undef,"$clientname room MYSENSORS_DEVICE");
$client = $defs{$clientname};
return unless ($client);
} else {
Log3($hash->{NAME},3,"MYSENSORS: ignoring presentation-msg from unknown radioId $msg->{radioId}, childId $msg->{childId}, sensorType $sensorType");
return;
}
}
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onPresentationMessage($client,$msg);
};
sub onSetMsg {
my ($hash,$msg) = @_;
if (my $client = matchClient($hash,$msg)) {
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onSetMessage($client,$msg);
}
Log3($hash->{NAME},3,"MYSENSORS: ignoring set-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType}));
return;
};
sub onRequestMsg {
my ($hash,$msg) = @_;
if (my $client = matchClient($hash,$msg)) {
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onRequestMessage($client,$msg);
}
Log3($hash->{NAME},3,"MYSENSORS: ignoring req-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType}));
return;
};
sub onInternalMsg {
my ($hash,$msg) = @_;
my $address = $msg->{radioId};
my $type = $msg->{subType};
my $client;
if ($address == 0 or $address == 255) { #msg to or from gateway
if ($type == I_INCLUSION_MODE) {
if (AttrVal($hash->{NAME},"autocreate",0)) { #if autocreate is switched on, keep gateways inclusion-mode active
if ($msg->{payload} == 0) {
sendMessage($hash,
radioId => $msg->{radioId},
childId => $msg->{childId},
ack => 0,
subType => I_INCLUSION_MODE,
payload => 1
);
}
} else {
$hash->{'inclusion-mode'} = $msg->{payload};
}
return;
}
if ($type == I_GATEWAY_READY) {
readingsSingleUpdate($hash,'connection','startup complete',1);
GP_ForallClients($hash,sub {
my $client = shift;
MYSENSORS::DEVICE::onGatewayStarted($client);
});
return InternalTimer(gettimeofday()+300, \&GetConnectStatus, $hash);
}
if ($type == I_HEARTBEAT_RESPONSE) {
RemoveInternalTimer($hash,\&Start); ## Reset reconnect because timeout was not reached
readingsSingleUpdate($hash, 'heartbeat', 'alive', 0);
if ($client = matchClient($hash,$msg)){
return if IsDisabled( $client->{NAME} );
MYSENSORS::DEVICE::onInternalMessage($client,$msg)
};
return;
}
if ($type == I_VERSION) {
$hash->{version} = $msg->{payload};
return;
}
if ($type == I_LOG_MESSAGE) {
return Log3($hash->{NAME},5,"MYSENSORS gateway $hash->{NAME}: $msg->{payload}");
}
if ($type == I_ID_REQUEST) {
if ($hash->{'inclusion-mode'}) {
my %nodes = map {$_ => 1} (AttrVal($hash->{NAME},"first-sensorid",20) ... AttrVal($hash->{NAME},"last-sensorid",254));
GP_ForallClients($hash,sub {
my $client = shift;
delete $nodes{$client->{radioId}};
});
if (keys %nodes) {
my $newid = (sort keys %nodes)[0];
sendMessage($hash,radioId => 255, childId => 255, cmd => C_INTERNAL, ack => 0, subType => I_ID_RESPONSE, payload => $newid);
Log3($hash->{NAME},4,"MYSENSORS $hash->{NAME} assigned new nodeid $newid");
} else {
Log3($hash->{NAME},4,"MYSENSORS $hash->{NAME} cannot assign new nodeid");
}
} else {
Log3($hash->{NAME},4,"MYSENSORS: ignoring id-request-msg from unknown radioId $msg->{radioId}");
}
return;
}
if ($type == I_TIME) {
if ($client = matchClient($hash,$msg)){
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onInternalMessage($client,$msg)
}
}
}
if ($client = matchClient($hash,$msg)) {
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onInternalMessage($client,$msg);
}
if ($client = matchChan76GWClient($hash,$msg)) {
Log3($hash->{NAME}, 4, "$hash->{NAME}: received stream message for $client - Chan76-IODev");
return MYSENSORS::DEVICE::onInternalMessage($client,$msg);
}
Log3($hash->{NAME},3,"MYSENSORS: ignoring internal-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".internalMessageTypeToStr($msg->{subType}));
return;
}
sub onStreamMsg {
my ($hash,$msg) = @_;
my $client;
if ($client = matchClient($hash, $msg)) {
Log3($hash->{NAME}, 4, "$hash->{NAME}: received stream message for $client - regular IODev");
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onStreamMessage($client, $msg);
}
if ($client = matchChan76GWClient($hash,$msg)) {
Log3($hash->{NAME}, 4, "$hash->{NAME}: received stream message for $client - Chan76-IODev");
return if IsDisabled( $client->{NAME} );
return MYSENSORS::DEVICE::onStreamMessage($client,$msg);
}
Log3($hash->{NAME},3,"MYSENSORS: ignoring stream-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".datastreamTypeToStr($msg->{subType}).". IO: $hash->{NAME}");
return;
}
sub onAcknowledge {
my ($hash,$msg) = @_;
my $ack;
if (defined (my $outstanding = $hash->{messagesForRadioId}->{$msg->{radioId}}->{messages})) {
my @remainMsg = grep {
$_->{childId} != $msg->{childId}
or $_->{cmd} != $msg->{cmd}
or $_->{subType} != $msg->{subType}
or $_->{payload} ne $msg->{payload}
} @$outstanding;
if ($ack = @remainMsg < @$outstanding) {
$hash->{outstandingAck} -= 1;
@$outstanding = @remainMsg;
}
$hash->{messagesForRadioId}->{$msg->{radioId}}->{numtries} = 1;
}
Log3 ($hash->{NAME},4,"MYSENSORS Read: unexpected ack ".dumpMsg($msg)) if !$ack;
return;
}
sub getFirmwareTypes {
my $hash = shift;
my $mode = shift // 0;
my $filename = shift // AttrVal($hash->{NAME}, 'OTA_firmwareConfig', undef);
my $name = $hash->{NAME};
return @{$hash->{'.fwList'}} if !$mode && defined $hash->{'.fwList'};
return 'OTA_firmwareConfig_not_valid_at_GW' if !$mode && !defined $hash->{'.fwList'};
my @fwTypes = ();
if (defined($filename)) {
my ($err, @lines) = FileRead({FileName => "./FHEM/firmware/$filename",
ForceType => 'file'});
if ( defined $err && $err ) {
Log3($name, 2, "$name: could not read MySensor firmware configuration file - $err");
return $err if $mode eq 'set' && $init_done;
return @fwTypes;
}
for (@lines) {
chomp($_);
if (index($_, "#") != 0) {
my @tokens = split m{,}xms, $_;
push @fwTypes, $tokens[0] if looks_like_number($tokens[0]);
}
}
$hash->{'.fwList'} = [@fwTypes];
}
delete $hash->{'.fwList'} if !@fwTypes;
Log3($name, 5, "$name: getFirmwareTypes - list contains: @fwTypes");
return if $mode eq 'set' && @fwTypes;
return 'file contains no valid data' if $mode eq 'set' && !@fwTypes;
return @fwTypes;
}
sub getLatestFirmware {
my $hash = shift;
my $type = shift // return;
my $name = $hash->{NAME};
my $cfgfilename = AttrVal($name, 'OTA_firmwareConfig', undef);
my $version = undef;
$name = undef;
my $filename = undef;
if (defined($cfgfilename)) {
my ($err, @lines) = FileRead({FileName => "./FHEM/firmware/$cfgfilename",
ForceType => 'file'});
if (defined($err) && $err) {
Log3($name, 2, "$name: could not read MySensor firmware configuration file - $err");
} else {
for (@lines) {
chomp($_);
if (index($_, '#') != 0) {
my @tokens = split m{,}xms, $_;
if ($tokens[0] eq $type) {
if ( !defined $version || $tokens[2] > $version ) {
$name = $tokens[1];
$version = $tokens[2];
$filename = $tokens[3];
}
}
}
}
}
}
return ($version, $filename, $name);
}
sub sendMessage {
my ($hash,%msg) = @_;
$msg{ack} = $hash->{ack} unless defined $msg{ack};
my $txt = createMsg(%msg);
Log3 ($hash->{NAME},5,"MYSENSORS send: ".dumpMsg(\%msg));
DevIo_SimpleWrite($hash,"$txt\n",undef);
if ($msg{ack}) {
my $messagesForRadioId = $hash->{messagesForRadioId}->{$msg{radioId}};
if (!defined $messagesForRadioId) {
$messagesForRadioId = {
lastseen => -1,
numtries => 1,
messages => [],
};
$hash->{messagesForRadioId}->{$msg{radioId}} = $messagesForRadioId;
}
my $messages = $messagesForRadioId->{messages};
@$messages = grep {
$_->{childId} != $msg{childId}
or $_->{cmd} != $msg{cmd}
or $_->{subType} != $msg{subType}
} @$messages;
push @$messages,\%msg;
$messagesForRadioId->{nexttry} = gettimeofday()+$messagesForRadioId->{numtries};
_scheduleTimer($hash);
}
return;
};
sub _scheduleTimer {
my $hash = shift;
$hash->{outstandingAck} = 0;
RemoveInternalTimer($hash,\&Timer);
my $next;
for my $radioid (keys %{$hash->{messagesForRadioId}}) {
my $msgsForId = $hash->{messagesForRadioId}->{$radioid};
$hash->{outstandingAck} += @{$msgsForId->{messages}};
$next = $msgsForId->{nexttry} if (!defined $next || $next >= $msgsForId->{nexttry});
};
InternalTimer($next, \&Timer, $hash, 0) if defined $next;
return;
}
sub matchClient {
my ($hash,$msg) = @_;
my $radioId = $msg->{radioId};
my $found;
GP_ForallClients($hash,sub {
return if $found;
my $client = shift;
if ($client->{radioId} == $radioId) {
$found = $client;
}
});
return $found;
}
sub matchChan76GWClient {
my ($hash,$msg) = @_;
my $radioId = $msg->{radioId};
my $name = $hash->{NAME};
my $found;
for my $d ( sort keys %defs ) {
if ( defined( $defs{$d} )
&& defined( $defs{$d}{radioId} )
&& $defs{$d}{radioId} == $radioId ) {
$found = $defs{$d} if AttrVal($d,'OTA_Chan76_IODev','') eq $name;
}
}
Log3($hash, 4, "$name: matched firmware config request to IO-name $found->{NAME}") if $found;
return $found;
}
sub parseMsg {
my $txt = shift // return;
use bytes;
return if ($txt !~ m{\A
(?<nodeid> [0-9]+);
(?<childid> [0-9]+);
(?<command> [0-4]);
(?<ack> [01]);
(?<type> [0-9]{1,2});
(?<payload> .*)
\z}xms);
#my $payload = $+{payload} // q{};
return {
radioId => $+{nodeid}, # docs speak of "nodeId"
childId => $+{childid},
cmd => $+{command},
ack => $+{ack},
subType => $+{type},
payload => $+{payload}
};
}
sub createMsg {
my %msgRef = @_;
my @fields = ( $msgRef{'radioId'} // -1,
$msgRef{'childId'} // -1,
$msgRef{'cmd'} // -1,
$msgRef{'ack'} // -1,
$msgRef{'subType'} // -1,
$msgRef{'payload'} // "");
return join(';', @fields);
}
sub dumpMsg {
my $msgRef = shift;
my $cmd = defined $msgRef->{'cmd'} ? commandToStr($msgRef->{'cmd'}) : "''";
my $st = (defined $msgRef->{'cmd'} and defined $msgRef->{'subType'}) ? subTypeToStr( $msgRef->{'cmd'}, $msgRef->{'subType'} ) : "''";
return sprintf("Rx: fr=%03d ci=%03d c=%03d(%-14s) st=%03d(%-16s) ack=%d %s\n", $msgRef->{'radioId'} // -1, $msgRef->{'childId'} // -1, $msgRef->{'cmd'} // -1, $cmd, $msgRef->{'subType'} // -1, $st, $msgRef->{'ack'} // -1, "'".($msgRef->{'payload'} // "")."'");
}
#our @EXPORT = ();
our @EXPORT_OK = (
commands,
variableTypes,
internalMessageTypes,
sensorTypes,
datastreamTypes,
payloadTypes,
qw(
sendMessage
getFirmwareTypes
getLatestFirmware
variableTypeToStr
variableTypeToIdx
internalMessageTypeToStr
sensorTypeToStr
sensorTypeToIdx
)
);
our %EXPORT_TAGS = (all => [@EXPORT_OK]);
1;
__END__
=pod
=item device
=item summary includes a MYSENSORS gateway
=item summary_DE integriert ein MYSENSORS Gateway
=begin html
<a id="MYSENSORS"></a>
<h3>MYSENSORS</h3>
<p>connects fhem to <a href="http://MYSENSORS.org">MYSENSORS</a>.</p>
<p>A single MYSENSORS device can serve multiple <a href="#MYSENSORS_DEVICE">MYSENSORS_DEVICE</a> clients.<br>
Each <a href="#MYSENSORS_DEVICE">MYSENSORS_DEVICE</a> represents a mysensors node.</p>
<a id="MYSENSORS-define"></a>
<h4>Define</h4>
<p><code>define &lt;name&gt; MYSENSORS &lt;serial device&gt;|&lt;ip:port&gt;</code></p>
<p>Specifies the MYSENSORS device.</p>
<a id="MYSENSORS-set"></a>
<h4>Set</h4>
<ul>
<li><a id="MYSENSORS-set-connect"></a>
<p><b>connect</b></p>
<p><code>set &lt;name&gt; connect</code></p>
<p>(re-)connects the MYSENSORS-device to the MYSENSORS-gateway</p>
<p>Can also be used to reload the <i>OTA_firmwareConfig</i> file after editing.</p>
</li>
<li><a id="MYSENSORS-set-disconnect"></a>
<p><b>disconnect</b></p>
<p><code>set &lt;name&gt; disconnect</code></p>
<p>disconnects the MYSENSORS-device from the MYSENSORS-gateway</p>
</li>
<li><a id="MYSENSORS-set-inclusion-mode"></a>
<p><b>inclusion-mode</b></p>
<p><code>set &lt;name&gt; inclusion-mode on|off</code></p>
<p>turns the gateways inclusion-mode on or off</p>
</li>
</ul>
<a id="MYSENSORS-attr"></a>
<h4>Attributes</h4>
<ul>
<li><a id="MYSENSORS-attr-autocreate"></a>
<p><b>autocreate</b></p>
<p><code>attr &lt;name&gt; autocreate</code></p>
<p>enables auto-creation of MYSENSOR_DEVICE-devices on receival of presentation-messages</p>
</li>
<li><a id="MYSENSORS-attr-requestAck"></a>
<p><b>requestAck</b></p>
<p><code>attr &lt;name&gt; requestAck</code></p>
<p>request acknowledge from nodes.</p>
<p>if set the Readings of nodes are updated not before requested acknowledge is received<br>
if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).</p>
<p>May also be configured for individual nodes if not set for gateway.</p>
</li>
<li><a id="MYSENSORS-attr-first-sensorid"></a>
<p><b>first-sensorid</b></p>
<p><code>attr &lt;name&gt; first-sensorid &lt;number &lt; 255&gt;</code></p>
<p>configures the lowest node-id assigned to a mysensor-node on request (defaults to 20)</p>
</li>
<li><a id="MYSENSORS-attr-OTA_firmwareConfig"></a>
<p><b>OTA_firmwareConfig</b></p>
<p><code>attr &lt;name&gt; OTA_firmwareConfig &lt;filename&gt;</code></p>
<p>specifies a configuration file for the <a href="https://www.mysensors.org/about/fota">FOTA</a>
(firmware over the air - wireless programming of the nodes) configuration. It must be stored
in the folder FHEM/firmware. The format of the configuration file is the following (csv):</p>
<p><code>#Type,Name,Version,File,Comments</code><br/>
<code>10,Blink,1,Blink.hex,blinking example</code></p>
<p>The meaning of the columns is the following:</p>
<dl>
<dt><code>Type</code></dt>
<dd>a numeric value (range 0 .. 65536) - each node will be assigned a firmware type</dd>
<dt><code>Name</code></dt>
<dd>a short name for this type</dd>
<dt><code>Version</code></dt>
<dd>a numeric value (range 0 .. 65536) - the version of the firmware (may be different
to the value that is send during the node presentation)</dd>
<dt><code>File</code></dt>
<dd>the filename containing the firmware - must also be stored in the folder FHEM/firmware</dd>
<dt><code>Comments</code></dt>
<dd>a description / comment for the firmware</dd>
</dl>
<p>Note: Firmware config file by default only is read at startup. If you change it later, just issue a <i>connect</i> command to update also Internal values.</p>
</li>
</ul>
=end html
=cut