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/fhem@18131 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
Beta-User 2019-01-04 11:58:28 +00:00
parent fb1b0de2b2
commit eb96fc307c
2 changed files with 827 additions and 333 deletions

View File

@ -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

View File

@ -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,6 +35,7 @@ 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} =
@ -54,6 +51,9 @@ sub MYSENSORS_DEVICE_Initialize($) {
"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
@ -202,14 +217,16 @@ sub Define($$) {
return "requires 1 parameters" unless (defined $radioId and $radioId ne "");
$hash->{radioId} = $radioId;
$hash->{sets} = {
'time' => "",
reboot => "",
# clear => "",
'time' => "noArg",
'reboot' => "noArg",
'clear' => "noArg",
'flash' => "noArg",
'fwType' => "",
};
$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);
};
@ -219,6 +236,7 @@ sub UnDefine($) {
my $name = $hash->{NAME};
RemoveInternalTimer("timeoutAck:$name");
RemoveInternalTimer("timeoutAlive:$name");
RemoveInternalTimer("timeoutAwake:$name");
return undef;
}
@ -226,23 +244,56 @@ sub Set($@) {
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 "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;
# };
$command eq "time" and do {
sendClientMessage($hash, childId => 255, cmd => C_INTERNAL, subType => I_TIME, payload => time);
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);
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 {
@ -270,14 +321,132 @@ sub Set($@) {
}
}
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;
};
@ -406,6 +575,9 @@ sub Attr($$$$) {
}
last;
};
$attribute eq "OTA_autoUpdate" and do {
last;
};
}
}
@ -430,7 +602,8 @@ sub onPresentationMessage($$) {
last;
};
};
CommandAttr(undef, "$name version $msg->{payload}");
$hash->{version} = $msg->{payload};
#CommandAttr(undef, "$name version $msg->{payload}");
};
my $readingMappings = $hash->{readingMappings};
@ -439,9 +612,25 @@ sub onPresentationMessage($$) {
my $idStr = ($id > 0 ? $id : "");
my @ret = ();
foreach my $type (@{$sensorMappings->{sends}}) {
next if (defined $readingMappings->{$id}->{$type});
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;
}
@ -452,7 +641,14 @@ sub onPresentationMessage($$) {
foreach my $type (@{$sensorMappings->{receives}}) {
my $typeMapping = $typeMappings->{$type};
my $typeStr = $typeMapping->{type};
next if (defined $hash->{sets}->{"$typeStr$idStr"});
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 $hash->{sets}->{"$typeStr$idStr"}) {
next unless $hash->{getCommentReadings} eq "2";
}
if ($hash->{IODev}->{'inclusion-mode'}) {
my @values = ();
if ($typeMapping->{range}) {
@ -462,7 +658,6 @@ sub onPresentationMessage($$) {
}
if (my $ret = CommandAttr(undef,"$name setReading_$typeStr$idStr".(@values ? " ".join (",",@values) : ""))) {
push @ret,$ret;
}
} else {
push @ret,"no setReading for $id, $typeStr";
}
@ -470,6 +665,7 @@ sub onPresentationMessage($$) {
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) = @_;
@ -480,7 +676,7 @@ sub onSetMessage($$) {
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};
#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");
};
@ -497,7 +693,7 @@ sub onRequestMessage($$) {
payload => ReadingsVal($hash->{NAME},$readingname,$val)
);
};
refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive};
#refreshInternalMySTimer($hash,"Alive") if $hash->{timeoutAlive};
Log3 ($hash->{NAME}, 4, "MYSENSORS_DEVICE $hash->{NAME}: ignoring C_REQ-message ".GP_Catch($@)) if $@;
}
@ -508,8 +704,11 @@ sub onInternalMessage($$) {
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}");
# 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 {
@ -542,7 +741,7 @@ sub onInternalMessage($$) {
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"));
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;
@ -565,17 +764,27 @@ sub onInternalMessage($$) {
last;
};
$type == I_SKETCH_NAME and do {
$hash->{$typeStr} = $msg->{payload};
#$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};
#$hash->{$typeStr} = $msg->{payload};
readingsSingleUpdate($hash, "SKETCH_VERSION", $msg->{payload}, 1);
last;
};
$type == I_REBOOT and do {
$hash->{$typeStr} = $msg->{payload};
#$hash->{$typeStr} = $msg->{payload};
last;
};
$type == I_GATEWAY_READY and do {
@ -583,35 +792,161 @@ sub onInternalMessage($$) {
last;
};
$type == I_REQUEST_SIGNING and do {
$hash->{$typeStr} = $msg->{payload};
#$hash->{$typeStr} = $msg->{payload};
last;
};
$type == I_GET_NONCE and do {
$hash->{$typeStr} = $msg->{payload};
#$hash->{$typeStr} = $msg->{payload};
last;
};
$type == I_GET_NONCE_RESPONSE and do {
#$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};
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 (($hash->{ack} or $hash->{IODev}->{ack}) and $hash->{timeoutAck}) ;
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 onStreamMessage($$) {
my ($hash, $msg) = @_;
}
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};
@ -622,7 +957,6 @@ sub rawToMappedReading($$$$) {
sub mappedReadingToRaw($$$) {
my ($hash,$reading,$value) = @_;
my $readingsMapping = $hash->{readingMappings};
foreach my $id (keys %$readingsMapping) {
my $readingTypesForId = $readingsMapping->{$id};
@ -640,6 +974,104 @@ sub mappedReadingToRaw($$$) {
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};
@ -657,6 +1089,12 @@ sub refreshInternalMySTimer($$) {
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";
}
}
@ -678,6 +1116,9 @@ sub timeoutMySTimer($) {
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;
}
}
@ -704,7 +1145,19 @@ sub timeoutMySTimer($) {
<p><b>Set</b></p>
<ul>
<li>
<p><code>set &lt;name&gt; clear</code><br/>clears routing-table of a repeater-node</p>
<p><code>set &lt;name&gt; clear</code><br/>clears MySensors EEPROM area and reboot (i.e. "factory" reset) - requires MY_SPECIAL_DEBUG</p>
</li>
<li>
<p><code>set &lt;name&gt; flash</code><br/>
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.</p>
</li>
<li>
<p><code>set &lt;name&gt; fwType &lt;value&gt;</code><br/>
assigns a firmware type to this node (must be a numeric value in the range 0 .. 65536).
Should be contained in the <a href="#MYSENSORSattrOTA_firmwareConfig">FOTA configuration
file</a>.</p>
</li>
<li>
<p><code>set &lt;name&gt; time</code><br/>sets time for nodes (that support it)</p>
@ -712,6 +1165,31 @@ sub timeoutMySTimer($) {
<li>
<p><code>set &lt;name&gt; reboot</code><br/>reboots a node (requires a bootloader that supports it).<br/>Attention: Nodes that run the standard arduino-bootloader will enter a bootloop!<br/>Dis- and reconnect the nodes power to restart in this case.</p>
</li>
</ul>
<a name="MYSENSORS_DEVICEget"></a>
<p><b>Get</b></p>
<ul>
<li>
<p><code>get &lt;name&gt; Extended_DEBUG</code><br/>
requires MY_SPECIAL_DEBUG</p>
retrieves the CPU frequency, CPU voltage and free memory of the sensor</p>
</li>
</ul>
<ul>
<li>
<p><code>get &lt;name&gt; ReadingsFromComment</code><br/>
rebuild reding names from comments of presentation messages if available</p>
After issuing this get check the log for changes. </p>
</li>
</ul>
<ul>
<li>
<p><code>get &lt;name&gt; RSSI</code><br/>
requires MY_SIGNAL_REPORT_ENABLED, not supported by all transportation layers</p>
delievers a set of Signal Quality information.</p>
</li>
</ul>
<a name="MYSENSORS_DEVICEattr"></a>
<p><b>Attributes</b></p>
@ -719,6 +1197,11 @@ sub timeoutMySTimer($) {
<li>
<p><code>attr &lt;name&gt; config [&lt;M|I&gt;]</code><br/>configures metric (M) or inch (I). Defaults to 'M'</p>
</li>
<li>
<p><code>attr &lt;name&gt; OTA_autoUpdate [&lt;0|1&gt;]</code><br/>
specifies whether an automatic update of the sensor node should be performed (1) during startup of the
node or not (0). Defaults to 0</p>
</li>
<li>
<p><code>attr &lt;name&gt; setCommands [&lt;command:reading:value&gt;]*</code><br/>configures one or more commands that can be executed by set.<br/>e.g.: <code>attr &lt;name&gt; setCommands on:switch_1:on off:switch_1:off</code><br/>if list of commands contains both 'on' and 'off' <a href="#setExtensions">set extensions</a> are supported</p>
</li>
@ -726,14 +1209,21 @@ sub timeoutMySTimer($) {
<p><code>attr &lt;name&gt; setReading_&lt;reading&gt; [&lt;value&gt;]*</code><br/>configures a reading that can be modified by set-command<br/>e.g.: <code>attr &lt;name&gt; setReading_switch_1 on,off</code></p>
</li>
<li>
<p><code>attr &lt;name&gt; mapReading_&lt;reading&gt; &lt;childId&gt; &lt;readingtype&gt; [&lt;value&gt;:&lt;mappedvalue&gt;]*</code><br/>configures the reading-name for a given childId and sensortype<br/>e.g.: <code>attr xxx mapReading_aussentemperatur 123 temperature</code></p>
<p><code>attr &lt;name&gt; mapReading_&lt;reading&gt; &lt;childId&gt; &lt;readingtype&gt; [&lt;value&gt;:&lt;mappedvalue&gt;]*</code><br/>configures the reading-name for a given childId and sensortype<br/>e.g.: <code>attr xxx mapReading_aussentemperatur 123 temperature</code>
or <code>attr xxx mapReading_leftwindow 10 status 1:closed 0:open</code>. See also mapReadingType for setting defaults for types without predefined defaults</p>
</li>
<li>
<p><code>att &lt;name&gt; requestAck</code><br/>request acknowledge from nodes.<br/>if set the Readings of nodes are updated not before requested acknowledge is received<br/>if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).<br/>May also be configured on the gateway for all nodes at once</p>
<p><code>attr &lt;name&gt; requestAck</code><br/>request acknowledge from nodes.<br/>if set the Readings of nodes are updated not before requested acknowledge is received<br/>if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).<br/>May also be configured on the gateway for all nodes at once</p>
</li>
<li>
<p><code>attr &lt;name&gt; mapReadingType_&lt;reading&gt; &lt;new reading name&gt; [&lt;value&gt;:&lt;mappedvalue&gt;]*</code><br/>configures reading type names that should be used instead of technical names<br/>e.g.: <code>attr xxx mapReadingType_LIGHT switch 0:on 1:off</code>to be used for mysensor Variabletypes that have no predefined defaults (yet)</p>
</li>
<li>
<p><code>attr &lt;name&gt; OTA_BL_Type &lt;either Optiboot or MYSBootloader&gt;*</code><br/>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</p>
</li>
<li>
<p><code>attr &lt;name&gt; OTA_Chan76_IODev </code><br/>As MYSBootloader per default uses nRF24 channel 76, you may specify a different IODev for OTA data using channel 76</p>
</li>
<li>
<p><code>attr &lt;name&gt; timeoutAck &lt;time in seconds&gt;*</code><br/>configures timeout to set device state to NACK in case not all requested acks are received</p>
</li>