From 8574c8f8051c76af1312797f13aca2bb3b9df3ee Mon Sep 17 00:00:00 2001 From: Beta-User <> Date: Fri, 4 Jan 2019 11:58:28 +0000 Subject: [PATCH] 10_MYSENSORS_DEVICE: change alive logic to heartbeat, add OTA feature, add more MySensors 2.0 API features git-svn-id: https://svn.fhem.de/fhem/trunk@18131 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 4 + fhem/FHEM/10_MYSENSORS_DEVICE.pm | 1156 +++++++++++++++++++++--------- 2 files changed, 827 insertions(+), 333 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index eb7df4d10..39826b8b0 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,9 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 10_MYSENSORS_DEVICE: make OTA feature available, + change battery name convention, + support MySensors API 2.0 features like + heartbeat and smartSleep - bugfix: 49_SSCam: fix problem with some older SMTP SSL module - bugfix: 73_AutoShuttersControl: fix typo in commandref - change: 49_SSCam: V8.3.1, change usage of older SMTP versions when Email diff --git a/fhem/FHEM/10_MYSENSORS_DEVICE.pm b/fhem/FHEM/10_MYSENSORS_DEVICE.pm index d47e08ff1..974c6c39e 100755 --- a/fhem/FHEM/10_MYSENSORS_DEVICE.pm +++ b/fhem/FHEM/10_MYSENSORS_DEVICE.pm @@ -3,7 +3,7 @@ # fhem bridge to MySensors (see http://mysensors.org) # # Copyright (C) 2014 Norbert Truchsess -# Copyright (C) 2018 Hauswart@forum.fhem.de +# Copyright (C) 2019 Hauswart@forum.fhem.de # # This file is part of fhem. # @@ -27,10 +27,6 @@ use strict; use warnings; -my %gets = ( - "version" => "", -); - sub MYSENSORS_DEVICE_Initialize($) { my $hash = shift @_; @@ -39,8 +35,9 @@ sub MYSENSORS_DEVICE_Initialize($) { $hash->{DefFn} = "MYSENSORS::DEVICE::Define"; $hash->{UndefFn} = "MYSENSORS::DEVICE::UnDefine"; $hash->{SetFn} = "MYSENSORS::DEVICE::Set"; + $hash->{GetFn} = "MYSENSORS::DEVICE::Get"; $hash->{AttrFn} = "MYSENSORS::DEVICE::Attr"; - + $hash->{AttrList} = "config:M,I " . "mode:node,repeater " . @@ -49,11 +46,14 @@ sub MYSENSORS_DEVICE_Initialize($) { "setReading_.+ " . "mapReadingType_.+ " . "mapReading_.+ " . - "requestAck:1 " . + "requestAck:1 " . "timeoutAck " . "timeoutAlive " . "IODev " . "showtime:0,1 " . + "OTA_autoUpdate:0,1 " . + "OTA_BL_Type:Optiboot,MYSBootloader " . + "OTA_Chan76_IODev " . $main::readingFnAttributes; main::LoadModule("MYSENSORS"); @@ -75,6 +75,9 @@ BEGIN { GP_Import(qw( AttrVal readingsSingleUpdate + readingsBeginUpdate + readingsEndUpdate + readingsBulkUpdate CommandAttr CommandDeleteAttr CommandDeleteReading @@ -82,11 +85,23 @@ BEGIN { Log3 SetExtensions ReadingsVal + ReadingsNum + FileRead InternalTimer RemoveInternalTimer )) }; +my %gets = ( + "version" => "noArg", + "heartbeat" => "noArg", + "presentation" => "noArg", + "RSSI" => "noArg", + "Extended_DEBUG" => "noArg", + "ReadingsFromComment" => "noArg", +); + + my %static_types = ( S_DOOR => { receives => [], sends => [V_TRIPPED,V_ARMED] }, # Door and window sensors S_MOTION => { receives => [], sends => [V_TRIPPED,V_ARMED] }, # Motion sensors @@ -197,87 +212,241 @@ my %static_mappings = ( ); sub Define($$) { - my ( $hash, $def ) = @_; - my ($name, $type, $radioId) = split("[ \t]+", $def); - return "requires 1 parameters" unless (defined $radioId and $radioId ne ""); - $hash->{radioId} = $radioId; - $hash->{sets} = { - 'time' => "", - reboot => "", -# clear => "", - }; - $hash->{ack} = 0; - $hash->{typeMappings} = {map {variableTypeToIdx($_) => $static_mappings{$_}} keys %static_mappings}; - $hash->{sensorMappings} = {map {sensorTypeToIdx($_) => $static_types{$_}} keys %static_types}; + my ( $hash, $def ) = @_; + my ($name, $type, $radioId) = split("[ \t]+", $def); + return "requires 1 parameters" unless (defined $radioId and $radioId ne ""); + $hash->{radioId} = $radioId; + $hash->{sets} = { + 'time' => "noArg", + 'reboot' => "noArg", + 'clear' => "noArg", + 'flash' => "noArg", + 'fwType' => "", + }; - $hash->{readingMappings} = {}; - AssignIoPort($hash); + $hash->{ack} = 0; + $hash->{typeMappings} = {map {variableTypeToIdx($_) => $static_mappings{$_}} keys %static_mappings}; + $hash->{sensorMappings} = {map {sensorTypeToIdx($_) => $static_types{$_}} keys %static_types}; + $hash->{readingMappings} = {}; + AssignIoPort($hash); }; sub UnDefine($) { - my ($hash) = @_; - my $name = $hash->{NAME}; - RemoveInternalTimer("timeoutAck:$name"); - RemoveInternalTimer("timeoutAlive:$name"); - return undef; + my ($hash) = @_; + my $name = $hash->{NAME}; + RemoveInternalTimer("timeoutAck:$name"); + RemoveInternalTimer("timeoutAlive:$name"); + RemoveInternalTimer("timeoutAwake:$name"); + return undef; } sub Set($@) { - my ($hash,$name,$command,@values) = @_; - return "Need at least one parameters" unless defined $command; - if(!defined($hash->{sets}->{$command})) { - my $list = join(" ", map {$hash->{sets}->{$_} ne "" ? "$_:$hash->{sets}->{$_}" : $_} sort keys %{$hash->{sets}}); - return grep (/(^on$)|(^off$)/,keys %{$hash->{sets}}) == 2 ? SetExtensions($hash, $list, $name, $command, @values) : "Unknown argument $command, choose one of $list"; - } - COMMAND_HANDLER: { -# $command eq "clear" and do { -# # Test 102 anstatt 255 :) und Log -# sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, subType => I_CHILDREN, payload => "C"); -# Log3 ($name,3,"MYSENSORS_DEVICE $name: clear"); -# # Test -# last; -# }; + my ($hash,$name,$command,@values) = @_; + return "Need at least one parameters" unless defined $command; + if(!defined($hash->{sets}->{$command})) { + $hash->{sets}->{fwType} = join(",", getFirmwareTypes($hash->{IODev})); + my $list = join(" ", map {$hash->{sets}->{$_} ne "" ? "$_:$hash->{sets}->{$_}" : $_} sort keys %{$hash->{sets}}); + $hash->{sets}->{fwType} = ""; + return grep (/(^on$)|(^off$)/,keys %{$hash->{sets}}) == 2 ? SetExtensions($hash, $list, $name, $command, @values) : "Unknown argument $command, choose one of $list"; + } + + COMMAND_HANDLER: { $command eq "time" and do { - sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, subType => I_TIME, payload => time); - last; + sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, ack => 0, subType => I_TIME, payload => time); + last; }; $command eq "reboot" and do { - sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, subType => I_REBOOT); - last; + my $blVersion = ReadingsVal($name, "BL_VERSION", ""); + sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, ack => 0, subType => I_REBOOT) if (defined($hash->{OTA_BL_Type}) or $blVersion eq "3.0"); + last; + }; + $command eq "clear" and do { + sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, ack => 0, subType => I_DEBUG, payload => "E"); + Log3 ($name,3,"MYSENSORS_DEVICE $name: clear"); + last; + }; + $command eq "flash" and do { + my $blVersion = ReadingsVal($name, "BL_VERSION", ""); + my $blType = AttrVal($name, "OTA_BL_Type", ""); + my $fwType = ReadingsNum($name, "FW_TYPE", -1); + if ($fwType == -1) { + Log3 ($name,3,"Firmware type not defined (FW_TYPE) for $name, update not started"); + return "$name: Firmware type not defined (FW_TYPE)"; + } elsif ($blVersion eq "3.0" or $blType eq "Optiboot") { + Log3 ($name,4,"Startet flashing Firmware: Optiboot method"); + return flashFirmware($hash, $fwType); + } elsif ($blType eq "MYSBootloader") { + $hash->{OTA_requested} = 1; + Log3 ($name,4,"Send reboot command to MYSBootloader node to start update"); + sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, ack => 1, subType => I_REBOOT); + } else { + return "$name: No valid OTA_BL_Type specified" if ($blVersion eq ""); + return "$name: Expected bootloader version 3.0 but found: $blVersion or specify a valid OTA_BL_Type"; + } + last; + }; + $command eq "fwType" and do { + if (@values == 1) { + my ($type) = @values; + if ($type =~ /^\d*$/) { + readingsSingleUpdate($hash, 'FW_TYPE', $type, 1); + } else { + return "fwType must be numeric"; + } + } + last; }; (defined ($hash->{setcommands}->{$command})) and do { - my $setcommand = $hash->{setcommands}->{$command}; - eval { - my ($type,$childId,$mappedValue) = mappedReadingToRaw($hash,$setcommand->{var},$setcommand->{val}); - sendClientMessage($hash, - childId => $childId, - cmd => C_SET, - subType => $type, - payload => $mappedValue, + my $setcommand = $hash->{setcommands}->{$command}; + eval { + my ($type,$childId,$mappedValue) = mappedReadingToRaw($hash,$setcommand->{var},$setcommand->{val}); + sendClientMessage($hash, + childId => $childId, + cmd => C_SET, + subType => $type, + payload => $mappedValue, ); - readingsSingleUpdate($hash,$setcommand->{var},$setcommand->{val},1) unless ($hash->{ack} or $hash->{IODev}->{ack}); - }; - return "$command not defined: ".GP_Catch($@) if $@; - last; + readingsSingleUpdate($hash,$setcommand->{var},$setcommand->{val},1) unless ($hash->{ack} or $hash->{IODev}->{ack}); + }; + return "$command not defined: ".GP_Catch($@) if $@; + last; }; my $value = @values ? join " ",@values : ""; eval { - my ($type,$childId,$mappedValue) = mappedReadingToRaw($hash,$command,$value); - sendClientMessage($hash, childId => $childId, cmd => C_SET, subType => $type, payload => $mappedValue); - readingsSingleUpdate($hash,$command,$value,1) unless ($hash->{ack} or $hash->{IODev}->{ack}); + my ($type,$childId,$mappedValue) = mappedReadingToRaw($hash,$command,$value); + sendClientMessage($hash, childId => $childId, cmd => C_SET, subType => $type, payload => $mappedValue); + readingsSingleUpdate($hash,$command,$value,1) unless ($hash->{ack} or $hash->{IODev}->{ack}); }; return "$command not defined: ".GP_Catch($@) if $@; - } + } +} + +sub Get($@) { + my ($hash, @a) = @_; + my $type = $hash->{TYPE}; + return "\"get $type\" needs at least one parameter" if(@a < 2); + if(!defined($hash->{gets}->{$a[1]})) { + if(!defined($gets{$a[1]})) { + my @cList = map { $_ =~ m/^(file|raw)$/ ? $_ : "$_:noArg" } sort keys %gets; + return "Unknown argument $a[1], choose one of " . join(" ", @cList); + } + } + my $command = $a[1]; + + COMMAND_HANDLER: { + $command eq "version" and do { + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_VERSION); + last; + }; + $command eq "heartbeat" and do { + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_HEARTBEAT_REQUEST); + last; + }; + $command eq "presentation" and do { + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_PRESENTATION); + last; + }; + $command eq "RSSI" and do { + $hash->{I_RSSI} = 1; + sendClientMessage($hash, cmd => C_INTERNAL, subType => I_SIGNAL_REPORT_REQUEST, ack => 0, payload => "R"); + last; + }; + $command eq "Extended_DEBUG" and do { + $hash->{I_DEBUG} = 1; + sendClientMessage($hash, cmd => C_INTERNAL, subType => I_DEBUG, ack => 0, payload => "F"); + last; + }; + $command eq "ReadingsFromComment" and do { + $hash->{getCommentReadings} = 1; + sendClientMessage($hash, cmd => C_INTERNAL, subType => I_PRESENTATION, ack => 0); + last; + }; + } + return undef; +} + +sub onStreamMessage($$) { + my ($hash, $msg) = @_; + my $name = $hash->{NAME}; + my $type = $msg->{subType}; + #my $typeStr = datastreamTypeToStr($type); + my $blType = AttrVal($name, "OTA_BL_Type", ""); + my $blVersion = hex(substr($msg->{payload}, 16, 2)) . "." . hex(substr($msg->{payload}, 18, 2)); + my $fwType = hex2Short(substr($msg->{payload}, 0, 4)); + + TYPE_HANDLER: { + $type == ST_FIRMWARE_CONFIG_REQUEST and do { + if (length($msg->{payload}) == 20) { + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'FW_TYPE', $fwType) if ($blType eq "Optiboot"); + readingsBulkUpdate($hash, 'FW_VERSION', hex2Short(substr($msg->{payload}, 4, 4))) if ($blType eq "Optiboot"); + readingsBulkUpdate($hash, 'FW_BLOCKS', hex2Short(substr($msg->{payload}, 8, 4))); + readingsBulkUpdate($hash, 'FW_CRC', hex2Short(substr($msg->{payload}, 12, 4))); + readingsBulkUpdate($hash, 'BL_VERSION', $blVersion); + readingsEndUpdate($hash, 1); + Log3($name, 4, "$name: received ST_FIRMWARE_CONFIG_REQUEST"); + if ((AttrVal($name, "OTA_autoUpdate", 0) == 1) && ($blVersion eq "3.0" or $blType eq "Optiboot")) { + Log3($name, 4, "$name: Optiboot BL, Node set to OTA_autoUpdate => calling firmware update procedure"); + flashFirmware($hash, $fwType); + } elsif ($blType eq "MYSBootloader" && $hash->{OTA_requested} == 1) { + Log3($name, 4, "$name: MYSBootloader asking for firmware update, calling firmware update procedure"); + $fwType = ReadingsVal($name, "FW_TYPE", "unknown"); + flashFirmware($hash, $fwType); + } + } else { + Log3($name, 2, "$name: Failed to parse ST_FIRMWARE_CONFIG_REQUEST - expected payload length 32 but retrieved ".length($msg->{payload})); + } + last; + }; + $type == ST_FIRMWARE_REQUEST and do { + last if ($msg->{ack}); + if (length($msg->{payload}) == 12) { + my $version = hex2Short(substr($msg->{payload}, 4, 4)); + my $block = hex2Short(substr($msg->{payload}, 8, 4)); + my $fromIndex = $block * 16; + my $payload = $msg->{payload}; + my @fwData = @{$hash->{FW_DATA}}; + Log3($name, 5, "$name: Firmware block request $block (type $fwType, version $version)"); + + for (my $index = $fromIndex; $index < $fromIndex + 16; $index++) { + $payload = $payload . sprintf("%02X", $fwData[$index]); + } + if (defined $hash->{OTA_Chan76_IODev}) { + sendMessage($hash->{OTA_Chan76_IODev}, radioId => $hash->{radioId}, childId => 255, ack => 0, cmd => C_STREAM, subType => ST_FIRMWARE_RESPONSE, payload => $payload); + } else { + sendClientMessage($hash, childId => 255, cmd => C_STREAM, ack => 0, subType => ST_FIRMWARE_RESPONSE, payload => $payload); + } + readingsSingleUpdate($hash, "state", "updating", 1) unless ($hash->{STATE} eq "updating"); + readingsSingleUpdate($hash, "state", "update done", 1) if ($block == 0); + if ($block == 0 && $blType ne "Optiboot") { + readingsSingleUpdate($hash, 'FW_VERSION', $version, 1); + delete $hash->{OTA_requested} if (defined $hash->{OTA_requested}); + } + } else { + Log3($name, 2, "$name: Failed to parse ST_FIRMWARE_REQUEST - expected payload length 12 but retrieved ".length($msg->{payload})); + } + last; + }; + $type == ST_SOUND and do { + #Approach for transfer sound and other stream data: + #https://forum.mysensors.org/topic/1668/sending-image-data-over-the-mysensors-network/11 + #https://youtu.be/soaE5X6_aRk + last; + }; + $type == ST_IMAGE and do { + #see remark to ST_Sound + last; + }; + } } sub Attr($$$$) { my ($command,$name,$attribute,$value) = @_; - my $hash = $main::defs{$name}; ATTRIBUTE_HANDLER: { $attribute eq "config" and do { if ($main::init_done) { - sendClientMessage($hash, cmd => C_INTERNAL, childId => 255, subType => I_CONFIG, payload => $command eq 'set' ? $value : "M"); + sendClientMessage($hash, cmd => C_INTERNAL, childId => 255, ack => 0, subType => I_CONFIG, payload => $command eq 'set' ? $value : "M"); } last; }; @@ -320,23 +489,23 @@ sub Attr($$$$) { last; }; $attribute =~ /^setReading_(.+)$/ and do { - if ($command eq "set") { + if ($command eq "set") { $hash->{sets}->{$1}= (defined $value) ? join(",",split ("[, \t]+",$value)) : ""; - } else { + } else { CommandDeleteReading(undef,"$hash->{NAME} $1"); delete $hash->{sets}->{$1}; - } - last; + } + last; }; $attribute =~ /^mapReadingType_(.+)/ and do { - my $type = variableTypeToIdx("V_$1"); - if ($command eq "set") { + my $type = variableTypeToIdx("V_$1"); + if ($command eq "set") { my @values = split ("[, \t]",$value); $hash->{typeMappings}->{$type}={ type => shift @values, val => {map {$_ =~ /^(.+):(.+)$/; $1 => $2} @values}, } - } else { + } else { if ($static_mappings{"V_$1"}) { $hash->{typeMappings}->{$type}=$static_mappings{"V_$1"}; } else { @@ -347,12 +516,12 @@ sub Attr($$$$) { foreach my $todelete (map {$readingMappings->{$_}->{name}} grep {$readingMappings->{$_}->{type} == $type} keys %$readingMappings) { CommandDeleteReading(undef,"$hash->{NAME} $todelete"); #TODO do propper remap of existing readings } - } - last; + } + last; }; $attribute =~ /^mapReading_(.+)/ and do { - my $readingMappings = $hash->{readingMappings}; - FIND: foreach my $id (keys %$readingMappings) { + my $readingMappings = $hash->{readingMappings}; + FIND: foreach my $id (keys %$readingMappings) { my $readingsForId = $readingMappings->{$id}; foreach my $type (keys %$readingsForId) { if (($readingsForId->{$type}->{name} // "") eq $1) { @@ -360,11 +529,11 @@ sub Attr($$$$) { unless (keys %$readingsForId) { delete $readingMappings->{$id}; } - last FIND; + last FIND; } } - } - if ($command eq "set") { + } + if ($command eq "set") { my ($id,$typeStr,@values) = split ("[, \t]",$value); my $typeMappings = $hash->{typeMappings}; if (my @match = grep {$typeMappings->{$_}->{type} eq $typeStr} keys %$typeMappings) { @@ -376,309 +545,581 @@ sub Attr($$$$) { } else { return "unknown reading type $typeStr"; } - } else { + } else { CommandDeleteReading(undef,"$hash->{NAME} $1"); - } - last; + } + last; }; $attribute eq "requestAck" and do { - if ($command eq "set") { + if ($command eq "set") { $hash->{ack} = 1; - } else { + } else { $hash->{ack} = 0; - } - last; + } + last; + }; + $attribute eq "timeoutAck" and do { + if ($command eq "set") { + $hash->{timeoutAck} = $value; + } else { + $hash->{timeoutAck} = 0; + } + last; + }; + $attribute eq "timeoutAlive" and do { + if ($command eq "set" and $value) { + $hash->{timeoutAlive} = $value; + refreshInternalMySTimer($hash,"Alive"); + } else { + $hash->{timeoutAlive} = 0; + } + last; + }; + $attribute eq "OTA_autoUpdate" and do { + last; }; - $attribute eq "timeoutAck" and do { - if ($command eq "set") { - $hash->{timeoutAck} = $value; - } else { - $hash->{timeoutAck} = 0; - } - last; - }; - $attribute eq "timeoutAlive" and do { - if ($command eq "set" and $value) { - $hash->{timeoutAlive} = $value; - refreshInternalMySTimer($hash,"Alive"); - } else { - $hash->{timeoutAlive} = 0; - } - last; - }; } } sub onGatewayStarted($) { - my ($hash) = @_; - refreshInternalMySTimer($hash,"Alive") if ($hash->{timeoutAlive}); + my ($hash) = @_; + refreshInternalMySTimer($hash,"Alive") if ($hash->{timeoutAlive}); } sub onPresentationMessage($$) { - my ($hash,$msg) = @_; - my $name = $hash->{NAME}; - my $nodeType = $msg->{subType}; - my $id = $msg->{childId}; - if ($id == 255) { #special id + my ($hash,$msg) = @_; + my $name = $hash->{NAME}; + my $nodeType = $msg->{subType}; + my $id = $msg->{childId}; + if ($id == 255) { #special id NODETYPE: { - $nodeType == S_ARDUINO_NODE and do { + $nodeType == S_ARDUINO_NODE and do { CommandAttr(undef, "$name mode node"); last; - }; - $nodeType == S_ARDUINO_REPEATER_NODE and do { + }; + $nodeType == S_ARDUINO_REPEATER_NODE and do { CommandAttr(undef, "$name mode repeater"); last; - }; + }; + }; + $hash->{version} = $msg->{payload}; + #CommandAttr(undef, "$name version $msg->{payload}"); }; - CommandAttr(undef, "$name version $msg->{payload}"); - }; - my $readingMappings = $hash->{readingMappings}; - my $typeMappings = $hash->{typeMappings}; - if (my $sensorMappings = $hash->{sensorMappings}->{$nodeType}) { - my $idStr = ($id > 0 ? $id : ""); + my $readingMappings = $hash->{readingMappings}; + my $typeMappings = $hash->{typeMappings}; + if (my $sensorMappings = $hash->{sensorMappings}->{$nodeType}) { + my $idStr = ($id > 0 ? $id : ""); my @ret = (); foreach my $type (@{$sensorMappings->{sends}}) { - next if (defined $readingMappings->{$id}->{$type}); - my $typeStr = $typeMappings->{$type}->{type}; - if ($hash->{IODev}->{'inclusion-mode'}) { - if (my $ret = CommandAttr(undef,"$name mapReading_$typeStr$idStr $id $typeStr")) { - push @ret,$ret; + if (defined $readingMappings->{$id}->{$type}) { + next unless defined $hash->{getCommentReadings}; + next unless $hash->{getCommentReadings} eq "2"; + } + my $typeStr = $typeMappings->{$type}->{type}; + if ($hash->{IODev}->{'inclusion-mode'}) { + if ($msg->{payload} ne "" and $hash->{getCommentReadings} eq "2") { + $idStr = "_".$msg->{payload}; + $idStr =~ s/\:/\./g; #replace illegal characters + $idStr =~ s/[^A-Za-z\d_\.-]+/_/g; + } + if (defined (my $mapping = $hash->{readingMappings}->{$id}->{$type})) { + if ($mapping->{name} ne "$typeStr$idStr" and $hash->{getCommentReadings} eq "2"and $msg->{payload} ne "") { + my $oldMappingName = $mapping->{name}; + CommandDeleteAttr(undef, "$hash->{NAME} mapReading_$oldMappingName"); + CommandDeleteReading(undef,"$hash->{NAME} $oldMappingName"); + Log3 ($hash->{NAME}, 3, "MYSENSORS_DEVICE $hash->{NAME}: Deleted Reading $oldMappingName"); + } + } + if (my $ret = CommandAttr(undef,"$name mapReading_$typeStr$idStr $id $typeStr")) { + push @ret,$ret; + } + } else { + push @ret,"no mapReading for $id, $typeStr"; } - } else { - push @ret,"no mapReading for $id, $typeStr"; - } } foreach my $type (@{$sensorMappings->{receives}}) { - my $typeMapping = $typeMappings->{$type}; - my $typeStr = $typeMapping->{type}; - next if (defined $hash->{sets}->{"$typeStr$idStr"}); - if ($hash->{IODev}->{'inclusion-mode'}) { - my @values = (); - if ($typeMapping->{range}) { - @values = ('slider',$typeMapping->{range}->{min},$typeMapping->{range}->{step},$typeMapping->{range}->{max}); - } elsif ($typeMapping->{val}) { - @values = values %{$typeMapping->{val}}; + my $typeMapping = $typeMappings->{$type}; + my $typeStr = $typeMapping->{type}; + if ($msg->{payload} ne "" and $hash->{getCommentReadings} eq "2") { + $idStr = "_".$msg->{payload}; + $idStr =~ s/\:/\./g; #replace illegal characters + $idStr =~ s/[^A-Za-z\d_\.-]+/_/g; } - if (my $ret = CommandAttr(undef,"$name setReading_$typeStr$idStr".(@values ? " ".join (",",@values) : ""))) { - push @ret,$ret; + if (defined $hash->{sets}->{"$typeStr$idStr"}) { + next unless $hash->{getCommentReadings} eq "2"; } - } else { - push @ret,"no setReading for $id, $typeStr"; - } + if ($hash->{IODev}->{'inclusion-mode'}) { + my @values = (); + if ($typeMapping->{range}) { + @values = ('slider',$typeMapping->{range}->{min},$typeMapping->{range}->{step},$typeMapping->{range}->{max}); + } elsif ($typeMapping->{val}) { + @values = values %{$typeMapping->{val}}; + } + if (my $ret = CommandAttr(undef,"$name setReading_$typeStr$idStr".(@values ? " ".join (",",@values) : ""))) { + push @ret,$ret; + } else { + push @ret,"no setReading for $id, $typeStr"; + } + } + Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: errors on C_PRESENTATION-message for childId $id, subType ".sensorTypeToStr($nodeType)." ".join (", ",@ret)) if @ret; + } } - Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: errors on C_PRESENTATION-message for childId $id, subType ".sensorTypeToStr($nodeType)." ".join (", ",@ret)) if @ret; - } } sub onSetMessage($$) { - my ($hash,$msg) = @_; - my $name = $hash->{NAME}; - if (defined $msg->{payload}) { - eval { - my ($reading,$value) = rawToMappedReading($hash,$msg->{subType},$msg->{childId},$msg->{payload}); - readingsSingleUpdate($hash, $reading, $value, 1); - }; - Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_SET-message ".GP_Catch($@)) if $@; - refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; - } else { - Log3 ($hash->{NAME}, 5, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_SET-message without payload"); - }; + my ($hash,$msg) = @_; + my $name = $hash->{NAME}; + if (defined $msg->{payload}) { + eval { + my ($reading,$value) = rawToMappedReading($hash,$msg->{subType},$msg->{childId},$msg->{payload}); + readingsSingleUpdate($hash, $reading, $value, 1); + }; + Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_SET-message ".GP_Catch($@)) if $@; + #refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; #deactivate in case of wanted reduction of alive to internal (heartbeat/battery/smartsleep) messages + } else { + Log3 ($hash->{NAME}, 5, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_SET-message without payload"); + }; } sub onRequestMessage($$) { - my ($hash,$msg) = @_; - eval { - my ($readingname,$val) = rawToMappedReading($hash, $msg->{subType}, $msg->{childId}, $msg->{payload}); - sendClientMessage($hash, - childId => $msg->{childId}, - cmd => C_SET, - subType => $msg->{subType}, - payload => ReadingsVal($hash->{NAME},$readingname,$val) - ); - }; - refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; - Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_REQ-message ".GP_Catch($@)) if $@; + my ($hash,$msg) = @_; + eval { + my ($readingname,$val) = rawToMappedReading($hash, $msg->{subType}, $msg->{childId}, $msg->{payload}); + sendClientMessage($hash, + childId => $msg->{childId}, + cmd => C_SET, + subType => $msg->{subType}, + payload => ReadingsVal($hash->{NAME},$readingname,$val) + ); + }; + #refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_REQ-message ".GP_Catch($@)) if $@; } sub onInternalMessage($$) { - my ($hash,$msg) = @_; - my $name = $hash->{NAME}; - my $type = $msg->{subType}; - my $typeStr = internalMessageTypeToStr($type); - INTERNALMESSAGE: { + my ($hash,$msg) = @_; + my $name = $hash->{NAME}; + my $type = $msg->{subType}; + my $typeStr = internalMessageTypeToStr($type); + INTERNALMESSAGE: { $type == I_BATTERY_LEVEL and do { - readingsSingleUpdate($hash, "batterylevel", $msg->{payload}, 1); - Log3 ($name, 4, "MYSENSORS_DEVICE $name: batterylevel $msg->{payload}"); - last; + # readingsSingleUpdate($hash, "batterylevel", $msg->{payload}, 1); + # Log3 ($name, 3, "MYSENSORS_DEVICE $name: batterylevel is deprecated and will be removed soon, use batteryPercent instead (Forum #87575)"); + readingsSingleUpdate($hash, "batteryPercent", $msg->{payload}, 1); + refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + Log3 ($name, 4, "MYSENSORS_DEVICE $name: batteryPercent $msg->{payload}"); + last; }; $type == I_TIME and do { - if ($msg->{ack}) { - Log3 ($name, 4, "MYSENSORS_DEVICE $name: response to time-request acknowledged"); - } else { - sendClientMessage($hash,cmd => C_INTERNAL, childId => 255, subType => I_TIME, payload => time); - Log3 ($name, 4, "MYSENSORS_DEVICE $name: update of time requested"); - } - last; + if ($msg->{ack}) { + Log3 ($name, 4, "MYSENSORS_DEVICE $name: response to time-request acknowledged"); + } else { + sendClientMessage($hash,cmd => C_INTERNAL, childId => 255, subType => I_TIME, payload => time); + Log3 ($name, 4, "MYSENSORS_DEVICE $name: update of time requested"); + } + last; }; $type == I_VERSION and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_ID_REQUEST and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_ID_RESPONSE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_INCLUSION_MODE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_CONFIG and do { - if ($msg->{ack}) { - Log3 ($name, 4, "MYSENSORS_DEVICE $name: response to config-request acknowledged"); - } else { - readingsSingleUpdate($hash, "parentId", $msg->{payload}, 1); - sendClientMessage($hash,cmd => C_INTERNAL, childId => 255, subType => I_CONFIG, payload => AttrVal($name,"config","M")); - Log3 ($name, 4, "MYSENSORS_DEVICE $name: respond to config-request, node parentId = " . $msg->{payload}); - } - last; + if ($msg->{ack}) { + Log3 ($name, 4, "MYSENSORS_DEVICE $name: response to config-request acknowledged"); + } else { + readingsSingleUpdate($hash, "parentId", $msg->{payload}, 1); + sendClientMessage($hash,cmd => C_INTERNAL, ack => 0, childId => 255, subType => I_CONFIG, payload => AttrVal($name,"config","M")); + Log3 ($name, 4, "MYSENSORS_DEVICE $name: respond to config-request, node parentId = " . $msg->{payload}); + } + last; }; $type == I_FIND_PARENT and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_FIND_PARENT_RESPONSE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_LOG_MESSAGE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_CHILDREN and do { - readingsSingleUpdate($hash, "state", "routingtable cleared", 1); - Log3 ($name, 3, "MYSENSORS_DEVICE $name: routingtable cleared"); - last; + readingsSingleUpdate($hash, "state", "routingtable cleared", 1); + Log3 ($name, 3, "MYSENSORS_DEVICE $name: routingtable cleared"); + last; }; $type == I_SKETCH_NAME and do { - $hash->{$typeStr} = $msg->{payload}; - readingsSingleUpdate($hash, "SKETCH_NAME", $msg->{payload}, 1); - last; + #$hash->{$typeStr} = $msg->{payload}; + readingsSingleUpdate($hash, "state", "received presentation", 1) unless ($hash->{STATE} eq "received presentation"); + readingsSingleUpdate($hash, "SKETCH_NAME", $msg->{payload}, 1); + #undef $hash->{FW_DATA}; # enable this to free memory? + delete $hash->{FW_DATA} if (defined $hash->{FW_DATA}); + if (defined $hash->{getCommentReadings}){ + if ($hash->{getCommentReadings} eq "1") { + $hash->{getCommentReadings} = 2 ; + }elsif ($hash->{getCommentReadings} eq "2") { + delete $hash->{getCommentReadings}; + } + } + last; }; $type == I_SKETCH_VERSION and do { - $hash->{$typeStr} = $msg->{payload}; - readingsSingleUpdate($hash, "SKETCH_VERSION", $msg->{payload}, 1); - last; + #$hash->{$typeStr} = $msg->{payload}; + readingsSingleUpdate($hash, "SKETCH_VERSION", $msg->{payload}, 1); + last; }; $type == I_REBOOT and do { - $hash->{$typeStr} = $msg->{payload}; - last; + #$hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_GATEWAY_READY and do { - $hash->{$typeStr} = $msg->{payload}; - last; + $hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_REQUEST_SIGNING and do { - $hash->{$typeStr} = $msg->{payload}; - last; + #$hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_GET_NONCE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + #$hash->{$typeStr} = $msg->{payload}; + last; }; $type == I_GET_NONCE_RESPONSE and do { - $hash->{$typeStr} = $msg->{payload}; - last; + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_HEARTBEAT_REQUEST and do { + #$hash->{$typeStr} = $msg->{payload}; + refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + last; + }; + $type == I_PRESENTATION and do { + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_DISCOVER_REQUEST and do { + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_DISCOVER_RESPONSE and do { + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_HEARTBEAT_RESPONSE and do { + readingsSingleUpdate($hash, "heartbeat", "last", 0); + refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_LOCKED and do { + $hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_REGISTRATION_REQUEST and do { + $hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_REGISTRATION_RESPONSE and do { + $hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_DEBUG and do { + last if ($msg->{ack}); + if ($hash->{I_DEBUG} == 1) { + readingsSingleUpdate($hash, "XDBG_CPU_FREQUENCY", $msg->{payload}, 1); + $hash->{I_DEBUG} = 2; + sendClientMessage($hash, childID => 255, cmd => C_INTERNAL, ack => 0, subType => I_DEBUG,payload => "V"); + } elsif ($hash->{I_DEBUG} == 2) { + readingsSingleUpdate($hash, "XDBG_CPU_VOLTAGE", $msg->{payload}, 1); + $hash->{I_DEBUG} = 3; + sendClientMessage($hash, childID => 255, cmd => C_INTERNAL, ack => 0, subType => I_DEBUG,payload => "M"); + } elsif ($hash->{I_DEBUG} == 3) { + readingsSingleUpdate($hash, "XDBG_FREE_MEMORY", $msg->{payload}, 1); + delete $hash->{I_DEBUG}; # if defined $hash->{I_DEBUG}; + } + last; + }; + $type == I_SIGNAL_REPORT_REVERSE and do { + #$hash->{$typeStr} = $msg->{payload}; + last; + }; + $type == I_SIGNAL_REPORT_RESPONSE and do { + last if ($msg->{ack}); + my $subSet = $hash->{I_RSSI}; + if ($subSet == 1) { + readingsSingleUpdate($hash, "R_RSSI_to_Parent", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 2; #$subSet; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "R!"); + } elsif ($subSet == 2) { + readingsSingleUpdate($hash, "R_RSSI_from_Parent", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 3; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "S"); + } elsif ($subSet == 3) { + readingsSingleUpdate($hash, "R_SNR_to_Parent", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 4; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "S!"); + } elsif ($subSet == 4) { + readingsSingleUpdate($hash, "R_SNR_from_Parent", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 5; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "P"); + } elsif ($subSet == 5) { + readingsSingleUpdate($hash, "R_TX_Powerlevel_Pct", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 6; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "T"); + } elsif ($subSet == 6) { + readingsSingleUpdate($hash, "R_TX_Powerlevel_dBm", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + $hash->{I_RSSI} = 7; + sendClientMessage($hash, cmd => C_INTERNAL, ack => 0, subType => I_SIGNAL_REPORT_REQUEST, payload => "U"); + } elsif ($subSet == 7) { + readingsSingleUpdate($hash, "R_Uplink_Quality", $msg->{payload}, 1) if ($msg->{payload} ne "-256" ); + delete $hash->{I_RSSI}; + } + last; + }; + $type == I_PRE_SLEEP_NOTIFICATION and do { + #$hash->{$typeStr} = $msg->{payload}; + refreshInternalMySTimer($hash,"Asleep"); + refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + #here we send out retained and outstanding messages + MYSENSORS::Timer($hash); + my $retainedMsg; + while (ref ($retainedMsg = shift @{$hash->{retainedMessagesForRadioId}->{messages}}) eq 'HASH') { + sendClientMessage($hash,%$retainedMsg); + }; + last; + }; + $type == I_POST_SLEEP_NOTIFICATION and do { + #$hash->{$typeStr} = $msg->{payload}; + readingsSingleUpdate($hash,"state","awake",1) unless ($hash->{STATE} eq "NACK"); + $hash->{nowSleeping} = 0; + refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive}; + last; }; } } sub sendClientMessage($%) { - my ($hash,%msg) = @_; - $msg{radioId} = $hash->{radioId}; - $msg{ack} = $hash->{ack} unless defined $msg{ack}; - sendMessage($hash->{IODev},%msg); - refreshInternalMySTimer($hash,"Ack") if (($hash->{ack} or $hash->{IODev}->{ack}) and $hash->{timeoutAck}) ; -} - -sub onStreamMessage($$) { - my ($hash, $msg) = @_; + my ($hash,%msg) = @_; + $msg{radioId} = $hash->{radioId}; + my $name = $hash->{NAME}; + $msg{ack} = $hash->{ack} unless defined $msg{ack}; + my $messages = $hash->{retainedMessagesForRadioId}->{messages}; + unless ($hash->{nowSleeping}) { + sendMessage($hash->{IODev},%msg); + refreshInternalMySTimer($hash,"Ack") if (($msg{ack} or $hash->{IODev}->{ack}) and $hash->{timeoutAck}); + Log3 ($name,5,"$name is not sleeping, sending message!"); + $hash->{retainedMessages}=scalar(@$messages) if (defined $hash->{retainedMessages}); + } else { + Log3 ($name,5,"$name is sleeping, enqueueing message! "); + #write to queue if node is asleep + unless (defined $hash->{retainedMessages}) { + $messages = {messages => [%msg]}; + $hash->{retainedMessages}=1; + Log3 ($name,5,"$name: No array yet for enqueued messages, building it!"); + } else { + #my $referencetype = ref $messages; + #Log3 ($name,4,"$name: Reference type is $referencetype."); + @$messages = grep { + $_->{childId} != $msg{childId} + or $_->{cmd} != $msg{cmd} + or $_->{subType} != $msg{subType} + } @$messages; + push @$messages,\%msg; + eval($hash->{retainedMessages}=scalar(@$messages)); + } + } } sub rawToMappedReading($$$$) { - my($hash, $type, $childId, $value) = @_; - - my $name; - if (defined (my $mapping = $hash->{readingMappings}->{$childId}->{$type})) { - my $val = $mapping->{val} // $hash->{typeMappings}->{$type}->{val}; - return ($mapping->{name},defined $val ? ($val->{$value} // $value) : $value); - } - die "no reading-mapping for childId $childId, type ".($hash->{typeMappings}->{$type}->{type} ? $hash->{typeMappings}->{$type}->{type} : variableTypeToStr($type)); + my($hash, $type, $childId, $value) = @_; + my $name; + if (defined (my $mapping = $hash->{readingMappings}->{$childId}->{$type})) { + my $val = $mapping->{val} // $hash->{typeMappings}->{$type}->{val}; + return ($mapping->{name},defined $val ? ($val->{$value} // $value) : $value); + } + die "no reading-mapping for childId $childId, type ".($hash->{typeMappings}->{$type}->{type} ? $hash->{typeMappings}->{$type}->{type} : variableTypeToStr($type)); } sub mappedReadingToRaw($$$) { - my ($hash,$reading,$value) = @_; - - my $readingsMapping = $hash->{readingMappings}; - foreach my $id (keys %$readingsMapping) { - my $readingTypesForId = $readingsMapping->{$id}; - foreach my $type (keys %$readingTypesForId) { - if (($readingTypesForId->{$type}->{name} // "") eq $reading) { - if (my $valueMappings = $readingTypesForId->{$type}->{val} // $hash->{typeMappings}->{$type}->{val}) { - if (my @mappedValues = grep {$valueMappings->{$_} eq $value} keys %$valueMappings) { - return ($type,$id,shift @mappedValues); + my ($hash,$reading,$value) = @_; + my $readingsMapping = $hash->{readingMappings}; + foreach my $id (keys %$readingsMapping) { + my $readingTypesForId = $readingsMapping->{$id}; + foreach my $type (keys %$readingTypesForId) { + if (($readingTypesForId->{$type}->{name} // "") eq $reading) { + if (my $valueMappings = $readingTypesForId->{$type}->{val} // $hash->{typeMappings}->{$type}->{val}) { + if (my @mappedValues = grep {$valueMappings->{$_} eq $value} keys %$valueMappings) { + return ($type,$id,shift @mappedValues); + } } + return ($type,$id,$value); } - return ($type,$id,$value); } } - } - die "no mapping for reading $reading"; + die "no mapping for reading $reading"; +} + +sub short2Hex($) { + my ($val) = @_; + my $temp = sprintf("%04X", $val); + return substr($temp, 2, 2) . substr($temp, 0, 2); +} + +sub hex2Short($) { + my ($val) = @_; + return hex(substr($val, 2, 2) . substr($val, 0, 2)); +} + +sub flashFirmware($$) { + my ($hash, $fwType) = @_; + my $name = $hash->{NAME}; + my ($version, $filename, $firmwarename) = getLatestFirmware($hash->{IODev}, $fwType); + if (not defined $filename) { + Log3 ($name,3,"No firmware defined for type $fwType - not flashing!"); + return "No firmware defined for type " . $fwType ; + } + my ($err, @lines) = FileRead({FileName => "./FHEM/firmware/" . $filename, ForceType => "file"}); + if (defined($err) && $err) { + Log3 ($name,3,"Could not read firmware file - $err: not flashing!"); + return "Could not read firmware file - $err"; + } else { + my $start = 0; + my $end = 0; + my @fwdata = (); + readingsSingleUpdate($hash, "state", "updating", 1) unless ($hash->{STATE} eq "updating"); + for (my $i = 0; $i < @lines ; $i++) { + chomp(my $row = $lines[$i]); + if (length($row) > 0) { + $row =~ s/^:+//; + my $reclen = hex(substr($row, 0, 2)); + my $offset = hex(substr($row, 2, 4)); + my $rectype = hex(substr($row, 6, 2)); + my $data = substr($row, 8, 2 * $reclen); + if ($rectype == 0) { + if (($start == 0) && ($end == 0)) { + if ($offset % 128 > 0) { + Log3 ($name,3,"error loading hex file - offset can't be devided by 128"); + return "error loading hex file - offset can't be devided by 128" ; + } + $start = $offset; + $end = $offset; + } + if ($offset < $end) { + Log3 ($name,3,"error loading hex file - offset can't be devided by 128"); + return "error loading hex file - offset lower than end" ; + } + while ($offset > $end) { + push(@fwdata, 255); + $end++; + } + for (my $i = 0; $i < $reclen; $i++) { + push(@fwdata, hex(substr($data, $i * 2, 2))); + } + $end += $reclen; + } + } + } + my $pad = $end % 128; # ATMega328 has 64 words per page / 128 bytes per page + for (my $i = 0; $i < 128 - $pad; $i++) { + push(@fwdata, 255); + $end++; + } + my $blocks = ($end - $start) / 16; + my $crc = 0xFFFF; + for (my $index = 0; $index < @fwdata; ++$index) { + $crc ^= $fwdata[$index] & 0xFF; + for (my $bit = 0; $bit < 8; ++$bit) { + if (($crc & 0x01) == 0x01) { + $crc = (($crc >> 1) ^ 0xA001); + } else { + $crc = ($crc >> 1); + } + } + } + if (($version != ReadingsNum($name, "FW_VERSION", -1)) || ($blocks != ReadingsNum($name, "FW_BLOCKS", -1)) || ($crc != ReadingsNum($name, "FW_CRC", -1))) { + Log3($name, 4, "$name: Flashing './FHEM/firmware/" . $filename . "'"); + $hash->{FW_DATA} = \@fwdata; + my $payload = short2Hex($fwType) . short2Hex($version) . short2Hex($blocks) . short2Hex($crc); + if (defined $hash->{OTA_Chan76_IODev}) { + sendMessage($hash->{OTA_Chan76_IODev}, radioId => $hash->{radioId}, childId => 255, ack => 0, cmd => C_STREAM, subType => ST_FIRMWARE_CONFIG_RESPONSE, payload => $payload); + Log3 ($name,5,"Directly send firmware info to $name using OTA_Chan76_IODev"); + } elsif (AttrVal($name, "OTA_BL_Type", "") eq "MYSBootloader") { + sendMessage($hash->{IODev}, radioId => $hash->{radioId}, childId => 255, ack => 0, cmd => C_STREAM, subType => ST_FIRMWARE_CONFIG_RESPONSE, payload => $payload); + Log3 ($name,5,"Directly send firmware info to $name using regular IODev"); + } else { + sendClientMessage($hash, childId => 255, cmd => C_STREAM, ack => 0, subType => ST_FIRMWARE_CONFIG_RESPONSE, payload => $payload); + Log3 ($name,5,"Send firmware info to $name using sendClientMessage"); + } + return undef; + } else { + return "Nothing todo - latest firmware already installed"; + } + } } sub refreshInternalMySTimer($$) { - my ($hash,$calltype) = @_; - my $name = $hash->{NAME}; - Log3 $name, 5, "$name: refreshInternalMySTimer called ($calltype)"; - if ($calltype eq "Alive") { - RemoveInternalTimer("timeoutAlive:$name"); - my $nextTrigger = main::gettimeofday() + $hash->{timeoutAlive}; - InternalTimer($nextTrigger, "MYSENSORS::DEVICE::timeoutMySTimer", "timeoutAlive:$name", 0); - if ($hash->{STATE} ne "NACK" or $hash->{STATE} eq "NACK" and @{$hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}} == 0) { - my $do_trigger = $hash->{STATE} ne "alive" ? 1 : 0; - readingsSingleUpdate($hash,"state","alive",$do_trigger); - } - } elsif ($calltype eq "Ack") { - RemoveInternalTimer("timeoutAck:$name"); - my $nextTrigger = main::gettimeofday() + $hash->{timeoutAck}; - InternalTimer($nextTrigger, "MYSENSORS::DEVICE::timeoutMySTimer", "timeoutAck:$name", 0); - Log3 $name, 4, "$name: Ack timeout timer set at $nextTrigger"; - } + my ($hash,$calltype) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "$name: refreshInternalMySTimer called ($calltype)"; + if ($calltype eq "Alive") { + RemoveInternalTimer("timeoutAlive:$name"); + my $nextTrigger = main::gettimeofday() + $hash->{timeoutAlive}; + InternalTimer($nextTrigger, "MYSENSORS::DEVICE::timeoutMySTimer", "timeoutAlive:$name", 0); + if ($hash->{STATE} ne "NACK" or $hash->{STATE} eq "NACK" and @{$hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}} == 0) { + my $do_trigger = $hash->{STATE} ne "alive" ? 1 : 0; + readingsSingleUpdate($hash,"state","alive",$do_trigger); + } + } elsif ($calltype eq "Ack") { + RemoveInternalTimer("timeoutAck:$name"); + my $nextTrigger = main::gettimeofday() + $hash->{timeoutAck}; + InternalTimer($nextTrigger, "MYSENSORS::DEVICE::timeoutMySTimer", "timeoutAck:$name", 0); + Log3 $name, 4, "$name: Ack timeout timer set at $nextTrigger"; + } elsif ($calltype eq "Asleep") { + RemoveInternalTimer("timeoutAwake:$name"); + #0.5 is default; could be dynamized by attribute if needed + my $nextTrigger = main::gettimeofday() + 0.3; + InternalTimer($nextTrigger, "MYSENSORS::DEVICE::timeoutMySTimer", "timeoutAwake:$name", 0); + Log3 $name, 5, "$name: Awake timeout timer set at $nextTrigger"; + } } sub timeoutMySTimer($) { - my ($calltype, $name) = split(':', $_[0]); - my $hash = $main::defs{$name}; - Log3 $name, 5, "$name: timeoutMySTimer called ($calltype)"; - if ($calltype eq "timeoutAlive") { - readingsSingleUpdate($hash,"state","dead",1) unless ($hash->{STATE} eq "NACK"); - } elsif ($calltype eq "timeoutAck") { - #readingsSingleUpdate($hash,"state","timeoutAck passed",1);# if ($hash->{STATE} eq "NACK"); - if ($hash->{IODev}->{outstandingAck} == 0) { - Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), no outstanding Acks at all"; - readingsSingleUpdate($hash,"state","alive",1) if ($hash->{STATE} eq "NACK"); - } elsif (@{$hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}}) { - Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), outstanding: $hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}"; - readingsSingleUpdate($hash,"state","NACK",1) ; - } else { - Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), no outstanding Acks for Node"; - readingsSingleUpdate($hash,"state","alive",1) if ($hash->{STATE} eq "NACK"); - } - } + my ($calltype, $name) = split(':', $_[0]); + my $hash = $main::defs{$name}; + Log3 $name, 5, "$name: timeoutMySTimer called ($calltype)"; + if ($calltype eq "timeoutAlive") { + readingsSingleUpdate($hash,"state","dead",1) unless ($hash->{STATE} eq "NACK"); + } elsif ($calltype eq "timeoutAck") { + #readingsSingleUpdate($hash,"state","timeoutAck passed",1);# if ($hash->{STATE} eq "NACK"); + if ($hash->{IODev}->{outstandingAck} == 0) { + Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), no outstanding Acks at all"; + readingsSingleUpdate($hash,"state","alive",1) if ($hash->{STATE} eq "NACK"); + } elsif (@{$hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}}) { + Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), outstanding: $hash->{IODev}->{messagesForRadioId}->{$hash->{radioId}}->{messages}"; + readingsSingleUpdate($hash,"state","NACK",1) ; + } else { + Log3 $name, 4, "$name: timeoutMySTimer called ($calltype), no outstanding Acks for Node"; + readingsSingleUpdate($hash,"state","alive",1) if ($hash->{STATE} eq "NACK"); + } + } elsif ($calltype eq "timeoutAwake") { + readingsSingleUpdate($hash,"state","asleep",1) unless ($hash->{STATE} eq "NACK"); + $hash->{nowSleeping} = 1; + } } 1; @@ -693,53 +1134,102 @@ sub timeoutMySTimer($) {
represents a mysensors sensor attached to a mysensor-node
-requires a MYSENSOR-device as IODev
- -Define
-define <name> MYSENSORS_DEVICE <Sensor-type> <node-id>
Specifies the MYSENSOR_DEVICE device.
Set
-set <name> clear
clears routing-table of a repeater-node
set <name> time
sets time for nodes (that support it)
set <name> reboot
reboots a node (requires a bootloader that supports it).
Attention: Nodes that run the standard arduino-bootloader will enter a bootloop!
Dis- and reconnect the nodes power to restart in this case.
Attributes
-attr <name> config [<M|I>]
configures metric (M) or inch (I). Defaults to 'M'
attr <name> setCommands [<command:reading:value>]*
configures one or more commands that can be executed by set.
e.g.: attr <name> setCommands on:switch_1:on off:switch_1:off
if list of commands contains both 'on' and 'off' set extensions are supported
attr <name> setReading_<reading> [<value>]*
configures a reading that can be modified by set-command
e.g.: attr <name> setReading_switch_1 on,off
attr <name> mapReading_<reading> <childId> <readingtype> [<value>:<mappedvalue>]*
configures the reading-name for a given childId and sensortype
e.g.: attr xxx mapReading_aussentemperatur 123 temperature
att <name> requestAck
request acknowledge from nodes.
if set the Readings of nodes are updated not before requested acknowledge is received
if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).
May also be configured on the gateway for all nodes at once
attr <name> mapReadingType_<reading> <new reading name> [<value>:<mappedvalue>]*
configures reading type names that should be used instead of technical names
e.g.: attr xxx mapReadingType_LIGHT switch 0:on 1:off
to be used for mysensor Variabletypes that have no predefined defaults (yet)
attr <name> timeoutAck <time in seconds>*
configures timeout to set device state to NACK in case not all requested acks are received
attr <name> timeoutAlive <time in seconds>*
configures timeout to set device state to alive or dead. If messages from node are received within timout spec, state will be alive, otherwise dead. If state is NACK (in case timeoutAck is also set), state will only be changed to alive, if there are no outstanding messages to be sent.
represents a mysensors sensor attached to a mysensor-node
+requires a MYSENSOR-device as IODev
+ +Define
+define <name> MYSENSORS_DEVICE <Sensor-type> <node-id>
Specifies the MYSENSOR_DEVICE device.
Set
+set <name> clear
clears MySensors EEPROM area and reboot (i.e. "factory" reset) - requires MY_SPECIAL_DEBUG
set <name> flash
+ Checks whether a newer firmware version is available. If a newer firmware version is
+ available the flash procedure is started. The sensor node must support FOTA for
+ this.
set <name> fwType <value>
+ assigns a firmware type to this node (must be a numeric value in the range 0 .. 65536).
+ Should be contained in the FOTA configuration
+ file.
set <name> time
sets time for nodes (that support it)
set <name> reboot
reboots a node (requires a bootloader that supports it).
Attention: Nodes that run the standard arduino-bootloader will enter a bootloop!
Dis- and reconnect the nodes power to restart in this case.
Get
+get <name> Extended_DEBUG
+ requires MY_SPECIAL_DEBUG
get <name> ReadingsFromComment
+ rebuild reding names from comments of presentation messages if available
get <name> RSSI
+ requires MY_SIGNAL_REPORT_ENABLED, not supported by all transportation layers
Attributes
+attr <name> config [<M|I>]
configures metric (M) or inch (I). Defaults to 'M'
attr <name> OTA_autoUpdate [<0|1>]
+ specifies whether an automatic update of the sensor node should be performed (1) during startup of the
+ node or not (0). Defaults to 0
attr <name> setCommands [<command:reading:value>]*
configures one or more commands that can be executed by set.
e.g.: attr <name> setCommands on:switch_1:on off:switch_1:off
if list of commands contains both 'on' and 'off' set extensions are supported
attr <name> setReading_<reading> [<value>]*
configures a reading that can be modified by set-command
e.g.: attr <name> setReading_switch_1 on,off
attr <name> mapReading_<reading> <childId> <readingtype> [<value>:<mappedvalue>]*
configures the reading-name for a given childId and sensortype
e.g.: attr xxx mapReading_aussentemperatur 123 temperature
+ or attr xxx mapReading_leftwindow 10 status 1:closed 0:open
. See also mapReadingType for setting defaults for types without predefined defaults
attr <name> requestAck
request acknowledge from nodes.
if set the Readings of nodes are updated not before requested acknowledge is received
if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).
May also be configured on the gateway for all nodes at once
attr <name> mapReadingType_<reading> <new reading name> [<value>:<mappedvalue>]*
configures reading type names that should be used instead of technical names
e.g.: attr xxx mapReadingType_LIGHT switch 0:on 1:off
to be used for mysensor Variabletypes that have no predefined defaults (yet)
attr <name> OTA_BL_Type <either Optiboot or MYSBootloader>*
For other bootloaders than Optiboot V3.0 OTA updates will only work if bootloader type is specified - MYSBootloader will reboot node if firmware update is started, so make sure, node will really recover
attr <name> OTA_Chan76_IODev
As MYSBootloader per default uses nRF24 channel 76, you may specify a different IODev for OTA data using channel 76
attr <name> timeoutAck <time in seconds>*
configures timeout to set device state to NACK in case not all requested acks are received
attr <name> timeoutAlive <time in seconds>*
configures timeout to set device state to alive or dead. If messages from node are received within timout spec, state will be alive, otherwise dead. If state is NACK (in case timeoutAck is also set), state will only be changed to alive, if there are no outstanding messages to be sent.