############################################## # # 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 . # # $Id$ # ############################################## my %sets = ( "connect" => [], "disconnect" => [], "inclusion-mode" => [qw(on off)], ); my %gets = ( "version" => "" ); my @clients = qw( MYSENSORS_DEVICE ); sub MYSENSORS_Initialize($) { my $hash = shift @_; require "$main::attr{global}{modpath}/FHEM/DevIo.pm"; # Provider $hash->{Clients} = join (':',@clients); $hash->{ReadyFn} = "MYSENSORS::Ready"; $hash->{ReadFn} = "MYSENSORS::Read"; # Consumer $hash->{DefFn} = "MYSENSORS::Define"; $hash->{UndefFn} = "MYSENSORS::Undef"; $hash->{SetFn} = "MYSENSORS::Set"; $hash->{AttrFn} = "MYSENSORS::Attr"; $hash->{NotifyFn} = "MYSENSORS::Notify"; my @attrList = qw( autocreate:1 requestAck:1 first-sensorid last-sensorid stateFormat OTA_firmwareConfig ); $hash->{AttrList} = $hash->{AttrList} = join(" ", @attrList) } package MYSENSORS; use Exporter ('import'); @EXPORT = (); @EXPORT_OK = qw( sendMessage getFirmwareTypes getLatestFirmware ); %EXPORT_TAGS = (all => [@EXPORT_OK]); use strict; use warnings; use GPUtils qw(:all); use Device::MySensors::Constants qw(:all); use Device::MySensors::Message qw(:all); BEGIN {GP_Import(qw( CommandDefine CommandModify CommandAttr gettimeofday readingsSingleUpdate DevIo_OpenDev DevIo_SimpleWrite DevIo_SimpleRead DevIo_CloseDev RemoveInternalTimer InternalTimer AttrVal Log3 FileRead ))}; 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, $def ) = @_; $hash->{NOTIFYDEV} = "global"; if ($main::init_done) { return Start($hash); } else { return undef; } } sub Undef($) { Stop(shift); return undef; } sub Set($@) { my ($hash, @a) = @_; return "Need at least one parameters" if(@a < 2); return "Unknown argument $a[1], choose one of " . join(" ", map {@{$sets{$_}} ? $_.':'.join ',', @{$sets{$_}} : $_} sort keys %sets) if(!defined($sets{$a[1]})); my $command = $a[1]; my $value = $a[2]; COMMAND_HANDLER: { $command eq "connect" and do { Start($hash); last; }; $command eq "disconnect" and do { Stop($hash); last; }; $command eq "inclusion-mode" and do { 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; last; }; }; } sub Attr($$$$) { my ($command,$name,$attribute,$value) = @_; my $hash = $main::defs{$name}; ATTRIBUTE_HANDLER: { $attribute eq "autocreate" and do { if ($main::init_done) { my $mode = $command eq "set" ? 1 : 0; sendMessage($hash,radioId => $hash->{radioId}, childId => $hash->{childId}, ack => 0, subType => I_INCLUSION_MODE, payload => $mode); $hash->{'inclusion-mode'} = $mode; } last; }; $attribute eq "requestAck" and do { if ($command eq "set") { $hash->{ack} = 1; } else { $hash->{ack} = 0; $hash->{messages} = {}; $hash->{outstandingAck} = 0; } last; }; $attribute eq "OTA_firmwareConfig" and do { last; }; } } sub Notify($$) { my ($hash,$dev) = @_; if( grep(m/^(INITIALIZED|REREADCFG)$/, @{$dev->{CHANGED}}) ) { Start($hash); } elsif( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) { } } sub Start($) { my $hash = shift; my ($dev) = split("[ \t]+", $hash->{DEF}); $hash->{DeviceName} = $dev; CommandAttr(undef, "$hash->{NAME} stateFormat connection") unless AttrVal($hash->{NAME},"stateFormat",undef); DevIo_CloseDev($hash); return DevIo_OpenDev($hash, 0, "MYSENSORS::Init"); } sub Stop($) { my $hash = shift; DevIo_CloseDev($hash); RemoveInternalTimer($hash); readingsSingleUpdate($hash,"connection","disconnected",1); } sub Ready($) { my $hash = shift; return DevIo_OpenDev($hash, 1, "MYSENSORS::Init") if($hash->{STATE} eq "disconnected"); if(defined($hash->{USBDev})) { my $po = $hash->{USBDev}; my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status; return ( $InBytes > 0 ); } } sub Init($) { my $hash = shift; 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); sendMessage($hash, radioId => 0, childId => 0, cmd => C_INTERNAL, ack => 0, subType => I_VERSION, payload => ''); return undef; } # GetConnectStatus sub GetConnectStatus($){ my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "MySensors: GetConnectStatus called ..."; #query heartbeat from gateway sendMessage($hash, radioId => 0, childId => 0, cmd => C_INTERNAL, ack => 0, subType => I_HEARTBEAT_REQUEST, payload => ''); # neuen Timer starten in einem konfigurierten Interval. InternalTimer(gettimeofday()+300, "MYSENSORS::GetConnectStatus", $hash);# Restart check in 5 mins again InternalTimer(gettimeofday()+5, "MYSENSORS::Start", $hash); #Start timer for reset if after 5 seconds RESPONSE is not received } sub Timer($) { my $hash = shift; my $now = time; foreach my $radioid (keys %{$hash->{messagesForRadioId}}) { my $msgsForId = $hash->{messagesForRadioId}->{$radioid}; if ($now > $msgsForId->{nexttry}) { foreach 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}; } } _scheduleTimer($hash); } sub Read { my ($hash) = @_; 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/) { my $txt; ($txt,$data) = split("\n", $data, 2); $txt =~ s/\r//; if (my $msg = parseMsg($txt)) { Log3 ($name,4,"MYSENSORS Read: ".dumpMsg($msg)); if ($msg->{ack}) { onAcknowledge($hash,$msg); } RemoveInternalTimer($hash,"MYSENSORS::GetConnectStatus"); InternalTimer(gettimeofday()+300, "MYSENSORS::GetConnectStatus", $hash);# Restart check in 5 mins again my $type = $msg->{cmd}; MESSAGE_TYPE: { $type == C_PRESENTATION and do { onPresentationMsg($hash,$msg); last; }; $type == C_SET and do { onSetMsg($hash,$msg); last; }; $type == C_REQ and do { onRequestMsg($hash,$msg); last; }; $type == C_INTERNAL and do { onInternalMsg($hash,$msg); last; }; $type == C_STREAM and do { onStreamMsg($hash,$msg); last; }; } } else { Log3 ($name,5,"MYSENSORS Read: ".$txt."is no parsable mysensors message"); } } $hash->{PARTIAL} = $data; return undef; }; 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 $main::defs{$clientname}; CommandDefine(undef,"$clientname MYSENSORS_DEVICE $msg->{radioId}"); CommandAttr(undef,"$clientname IODev $hash->{NAME}"); CommandAttr(undef,"$clientname room MYSENSORS_DEVICE"); $client = $main::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; } } MYSENSORS::DEVICE::onPresentationMessage($client,$msg); }; sub onSetMsg($$) { my ($hash,$msg) = @_; if (my $client = matchClient($hash,$msg)) { MYSENSORS::DEVICE::onSetMessage($client,$msg); } else { Log3($hash->{NAME},3,"MYSENSORS: ignoring set-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType})); } }; sub onRequestMsg($$) { my ($hash,$msg) = @_; if (my $client = matchClient($hash,$msg)) { MYSENSORS::DEVICE::onRequestMessage($client,$msg); } else { Log3($hash->{NAME},3,"MYSENSORS: ignoring req-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType})); } }; sub onInternalMsg($$) { my ($hash,$msg) = @_; my $address = $msg->{radioId}; my $type = $msg->{subType}; if ($address == 0 or $address == 255) { #msg to or from gateway TYPE: { $type == I_INCLUSION_MODE and do { 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}; } last; }; $type == I_GATEWAY_READY and do { readingsSingleUpdate($hash,'connection','startup complete',1); GP_ForallClients($hash,sub { my $client = shift; MYSENSORS::DEVICE::onGatewayStarted($client); }); InternalTimer(gettimeofday()+300, "MYSENSORS::GetConnectStatus", $hash); last; }; $type == I_HEARTBEAT_RESPONSE and do { RemoveInternalTimer($hash,"MYSENSORS::Start"); ## Reset reconnect because timeout was not reached readingsSingleUpdate($hash, "heartbeat", "alive", 0); if (my $client = matchClient($hash,$msg)){ MYSENSORS::DEVICE::onInternalMessage($client,$msg) }; }; $type == I_VERSION and do { $hash->{version} = $msg->{payload}; last; }; $type == I_LOG_MESSAGE and do { Log3($hash->{NAME},5,"MYSENSORS gateway $hash->{NAME}: $msg->{payload}"); last; }; $type == I_ID_REQUEST and do { 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}"); } last; }; $type == I_TIME and do { if (my $client = matchClient($hash,$msg)){ MYSENSORS::DEVICE::onInternalMessage($client,$msg) } last; }; } } elsif (my $client = matchClient($hash,$msg)) { MYSENSORS::DEVICE::onInternalMessage($client,$msg); } elsif ($client = matchChan76GWClient($hash,$msg)) { Log3($hash->{NAME}, 4, "$hash->{NAME}: received stream message for $client - Chan76-IODev"); MYSENSORS::DEVICE::onInternalMessage($client,$msg); } else { Log3($hash->{NAME},3,"MYSENSORS: ignoring internal-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".internalMessageTypeToStr($msg->{subType})); } }; 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"); MYSENSORS::DEVICE::onStreamMessage($client, $msg); } elsif ($client = matchChan76GWClient($hash,$msg)) { Log3($hash->{NAME}, 4, "$hash->{NAME}: received stream message for $client - Chan76-IODev"); MYSENSORS::DEVICE::onStreamMessage($client,$msg); } else { Log3($hash->{NAME},3,"MYSENSORS: ignoring stream-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".datastreamTypeToStr($msg->{subType})); } } 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)) unless $ack; } sub getFirmwareTypes($) { my ($hash) = @_; my $name = $hash->{NAME}; my @fwTypes = (); my $filename = AttrVal($name, "OTA_firmwareConfig", undef); 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"); } else { for (my $i = 0; $i < @lines ; $i++) { chomp(my $row = $lines[$i]); if (index($row, "#") != 0) { my @tokens = split(",", $row); push(@fwTypes, $tokens[0]); } } } } Log3($name, 5, "$name: getFirmwareTypes - list contains: @fwTypes"); return @fwTypes; } sub getLatestFirmware($$) { my ($hash, $type) = @_; 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 (my $i = 0; $i < @lines ; $i++) { chomp(my $row = $lines[$i]); if (index($row, "#") != 0) { my @tokens = split(",", $row); if ($tokens[0] eq $type) { if ((not 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}}; unless (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); } }; sub _scheduleTimer($) { my ($hash) = @_; $hash->{outstandingAck} = 0; RemoveInternalTimer($hash,"MYSENSORS::Timer"); my $next; foreach my $radioid (keys %{$hash->{messagesForRadioId}}) { my $msgsForId = $hash->{messagesForRadioId}->{$radioid}; $hash->{outstandingAck} += @{$msgsForId->{messages}}; $next = $msgsForId->{nexttry} unless (defined $next and $next < $msgsForId->{nexttry}); }; InternalTimer($next, "MYSENSORS::Timer", $hash, 0) if (defined $next); } 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 $found; foreach my $d ( sort keys %main::defs ) { if ( defined( $main::defs{$d} ) && defined( $main::defs{$d}{radioId} ) && $main::defs{$d}{radioId} == $radioId ) { my $clientname = $main::defs{$d}->{NAME}; my $name = $hash->{NAME}; $found = $main::defs{$d} if AttrVal($clientname,"OTA_Chan76_IODev","") eq $name; } } Log3($hash->{NAME}, 4, "$hash->{NAME}: matched firmware config request to hash $found, name: $found->{NAME}") if $found; return $found if $found; return undef; } 1; =pod =item device =item summary includes a MYSENSORS gateway =item summary_DE integriert ein MYSENSORS Gateway =begin html

MYSENSORS

=end html =cut