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($) {

MYSENSORS_DEVICE