mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-02-26 04:24:53 +00:00
952 lines
32 KiB
Perl
Executable File
952 lines
32 KiB
Perl
Executable File
##############################################
|
|
# $Id$
|
|
# Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013
|
|
package main;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use MIME::Base64;
|
|
use POSIX;
|
|
use MaxCommon;
|
|
|
|
sub MAXLAN_Parse($$);
|
|
sub MAXLAN_Read($);
|
|
sub MAXLAN_Write(@);
|
|
sub MAXLAN_ReadSingleResponse($$);
|
|
sub MAXLAN_SimpleWrite(@);
|
|
sub MAXLAN_Poll($);
|
|
sub MAXLAN_Send(@);
|
|
sub MAXLAN_RequestConfiguration($$);
|
|
sub MAXLAN_RemoveDevice($$);
|
|
|
|
my $reconnect_interval = 60; #seconds
|
|
|
|
#the time it takes after sending one command till we see its effect in the L: response
|
|
my $roundtriptime = 3; #seconds
|
|
|
|
my $read_timeout = 3; #seconds. How long to wait for an answer from the Cube over TCP/IP
|
|
|
|
my $metadata_magic = 0x56;
|
|
my $metadata_version = 2;
|
|
|
|
my $defaultPollInterval = 60;
|
|
|
|
sub
|
|
MAXLAN_Initialize($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
require "$attr{global}{modpath}/FHEM/DevIo.pm";
|
|
|
|
# Provider
|
|
$hash->{ReadFn} = "MAXLAN_Read";
|
|
$hash->{SetFn} = "MAXLAN_Set";
|
|
$hash->{Clients} = ":MAX:";
|
|
my %mc = (
|
|
"1:MAX" => "^MAX",
|
|
);
|
|
$hash->{MatchList} = \%mc;
|
|
|
|
# Normal devices
|
|
$hash->{DefFn} = "MAXLAN_Define";
|
|
$hash->{UndefFn} = "MAXLAN_Undef";
|
|
$hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 set-clock-on-init:1,0 " .
|
|
"loglevel:0,1,2,3,4,5,6 addvaltrigger " .
|
|
"timezone:CET-CEST,GMT-BST,EET-EEST,FET-FEST,MSK-MSD,GMT,CET,EET";
|
|
}
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Define($$)
|
|
{
|
|
my ($hash, $def) = @_;
|
|
my @a = split("[ \t][ \t]*", $def);
|
|
|
|
if(@a < 3) {
|
|
my $msg = "wrong syntax: define <name> MAXLAN ip[:port] [pollintervall [ondemand]]";
|
|
Log 2, $msg;
|
|
return $msg;
|
|
}
|
|
|
|
my $name = shift @a;
|
|
shift @a;
|
|
my $dev = shift @a;
|
|
$dev .= ":62910" if($dev !~ m/:/ && $dev ne "none" && $dev !~ m/\@/);
|
|
|
|
if($dev eq "none") {
|
|
Log 1, "$name device is none, commands will be echoed only";
|
|
$attr{$name}{dummy} = 1;
|
|
return undef;
|
|
}
|
|
$hash->{INTERVAL} = $defaultPollInterval;
|
|
$hash->{persistent} = 1;
|
|
if(@a) {
|
|
$hash->{INTERVAL} = shift @a;
|
|
while(@a) {
|
|
my $arg = shift @a;
|
|
if($arg eq "ondemand") {
|
|
$hash->{persistent} = 0;
|
|
} else {
|
|
my $msg = "unknown argument $arg";
|
|
Log 1, $msg;
|
|
return $msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
$hash->{cubeTimeDifference} = 99999;
|
|
$hash->{pairmode} = 0;
|
|
$hash->{PARTIAL} = "";
|
|
$hash->{DeviceName} = $dev;
|
|
#This interface is shared with 14_CUL_MAX.pm
|
|
$hash->{Send} = \&MAXLAN_Send;
|
|
$hash->{RemoveDevice} = \&MAXLAN_RemoveDevice;
|
|
|
|
#Wait until all device definitions have been loaded
|
|
InternalTimer(gettimeofday()+1, "MAXLAN_Poll", $hash, 0);
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
MAXLAN_IsConnected($)
|
|
{
|
|
return 0 if(!exists($_[0]->{FD}));
|
|
if(!defined($_[0]->{TCPDev})) {
|
|
MAXLAN_Disconnect($_[0]);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
#Disconnects from the Cube. It is safe to call this when already disconnected.
|
|
sub
|
|
MAXLAN_Disconnect($)
|
|
{
|
|
my $hash = shift;
|
|
Log 5, "MAXLAN_Disconnect";
|
|
#All operations here are no-op if already disconnected
|
|
DevIo_CloseDev($hash);
|
|
RemoveInternalTimer($hash);
|
|
}
|
|
|
|
#Connects to the Cube. If already connected, disconnects first.
|
|
#Returns undef of success, otherwise an error message
|
|
sub
|
|
MAXLAN_Connect($)
|
|
{
|
|
my $hash = shift;
|
|
|
|
return undef if(MAXLAN_IsConnected($hash));
|
|
|
|
delete($hash->{NEXT_OPEN}); #work around the connection rate limiter in DevIo
|
|
DevIo_OpenDev($hash, 0, "");
|
|
if(!MAXLAN_IsConnected($hash)) {
|
|
my $msg = "MAXLAN_Connect: Could not connect";
|
|
Log 2, $msg;
|
|
return $msg;
|
|
}
|
|
|
|
my $ret;
|
|
#Read initial configuration data
|
|
$ret = MAXLAN_ExpectAnswer($hash,"H:");
|
|
return "MAXLAN_Connect: $ret" if($ret);
|
|
$ret = MAXLAN_ExpectAnswer($hash,"M:");
|
|
return "MAXLAN_Connect: $ret" if($ret);
|
|
|
|
#We first reset the IODev for all MAX devices using this MAXLAN as a backend.
|
|
#Parsing the "C:" responses later on will set IODev correctly again.
|
|
#This effectively removes IODev from all devices that are not longer paired to our Cube.
|
|
foreach (%{$modules{MAX}{defptr}}) {
|
|
$modules{MAX}{defptr}{$_}{IODev} = undef if(defined($modules{MAX}{defptr}{$_}{IODev}) and $modules{MAX}{defptr}{$_}{IODev} == $hash);
|
|
}
|
|
|
|
my $rmsg;
|
|
do
|
|
{
|
|
#Receive one "C:" per device
|
|
$rmsg = MAXLAN_ReadSingleResponse($hash, 1);
|
|
return "MAXLAN_Connect: Error in ReadSingleResponse while waiting for C:" if(!defined($rmsg));
|
|
MAXLAN_Parse($hash, $rmsg);
|
|
} until($rmsg =~ m/^L:/);
|
|
#At the end, the cube sends a "L:"
|
|
|
|
#Handle deferred setting of time
|
|
if(AttrVal($hash->{NAME},"set-clock-on-init","1") && $hash->{cubeTimeDifference} > 1) {
|
|
MAXLAN_Set($hash,$hash->{NAME},"clock");
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Undef($$)
|
|
{
|
|
my ($hash, $arg) = @_;
|
|
#MAXLAN_Write($hash,"q:"); #unnecessary
|
|
MAXLAN_Disconnect($hash);
|
|
return undef;
|
|
}
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Set($@)
|
|
{
|
|
my ($hash, $device, @a) = @_;
|
|
return "\"set MAXLAN\" needs at least one parameter" if(@a < 1);
|
|
my ($setting, @args) = @a;
|
|
|
|
if($setting eq "pairmode"){
|
|
if(@args > 0 and $args[0] eq "cancel") {
|
|
MAXLAN_Write($hash,"x:", "N:");
|
|
} else {
|
|
my $duration = 60;
|
|
$duration = $args[0] if(@args > 0);
|
|
$hash->{pairmode} = 1;
|
|
MAXLAN_Write($hash,"n:".sprintf("%04x",$duration));
|
|
$hash->{STATE} = "pairing";
|
|
}
|
|
|
|
}elsif($setting eq "raw"){
|
|
MAXLAN_Write($hash,$args[0]);
|
|
|
|
}elsif($setting eq "clock") {
|
|
#Set timezone from attribute
|
|
#All strings are taken from MAX! software network analysis
|
|
#Base64 hex decode of the CET strings gives eg.
|
|
#CET[00][00][0a][00][03][00][00][0e][10]CEST[00][03][00][02][00][00][1c][20] for DST
|
|
#CET[00][00][0a][00][03][00][00][0e][10]CEST[00][03][00][02][00][00][0e][10] for no DST
|
|
#bytes 10-11 and 22-23 of each string appear to represent time offset from UTC in seconds
|
|
#a guess is that bytes 5 & 17 represent month no.
|
|
#All strings below appear to follow the same pattern & identical except for name & offset.
|
|
#The currently set string appears at the end of the decoded C: device message for the Cube
|
|
my $timezoneAttr = AttrVal($hash->{NAME},"timezone","CET-CEST");
|
|
my %tz_list = ( #timezone & strings
|
|
"GMT-BST" => "R01UAAAKAAMAAAAAQlNUAAADAAIAAA4Q", #DST strings
|
|
"CET-CEST" => "Q0VUAAAKAAMAAA4QQ0VTVAADAAIAABwg",
|
|
"EET-EEST" => "RUVUAAAKAAMAABwgRUVTVAADAAIAACow",
|
|
"FET-FEST" => "RkVUAAAKAAMAACowRkVTVAADAAIAACow", #No DST for this region or next
|
|
"MSK-MSD" => "TVNLAAAKAAMAADhATVNEAAADAAIAADhA",
|
|
"GMT" => "R01UAAAKAAMAAAAAQlNUAAADAAIAAAAA", #No DST strings
|
|
"CET" => "Q0VUAAAKAAMAAA4QQ0VTVAADAAIAAA4Q",
|
|
"EET" => "RUVUAAAKAAMAABwgRUVTVAADAAIAABwg"
|
|
);
|
|
|
|
my $timezones;
|
|
if(exists($tz_list{$timezoneAttr})) {
|
|
$timezones = $tz_list{$timezoneAttr};
|
|
Log 3, "MAX Cube is set to timezone $timezoneAttr";
|
|
} else {
|
|
Log 2, "ERROR: Timezone $timezoneAttr of MAX Cube is invalid. Using CET-CEST";
|
|
$timezones = $tz_list{"CET-CEST"};
|
|
}
|
|
|
|
#From various sources Cube base time is year 2000, offset should perhaps be number
|
|
#of secs diff between 1/Jan/1970 and 1/Jan/2000 ie. 946684800, ie. 26 secs diff
|
|
#Occasional 1 min diffs seen in logs when close to minute rollover
|
|
my $time = time()-946684800;
|
|
my $rmsg = "v:".$timezones.",".sprintf("%08x",$time);
|
|
my $ret = MAXLAN_Write($hash,$rmsg, "A:");
|
|
$hash->{clockset} = 1;
|
|
return $ret;
|
|
|
|
}elsif($setting eq "factoryReset") {
|
|
MAXLAN_RequestReset($hash);
|
|
|
|
}elsif($setting eq "reconnect") {
|
|
MAXLAN_Disconnect($hash);
|
|
MAXLAN_Connect($hash) if($hash->{persistent});
|
|
|
|
}elsif($setting eq "inject") {
|
|
MAXLAN_Parse($hash,$args[0]);
|
|
|
|
}else{
|
|
return "Unknown argument $setting, choose one of pairmode raw clock factoryReset reconnect";
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
#Returns error string if failed, undef on success
|
|
sub
|
|
MAXLAN_ExpectAnswer($$)
|
|
{
|
|
my ($hash,$expectedanswer) = @_;
|
|
my $rmsg = MAXLAN_ReadSingleResponse($hash, 1);
|
|
|
|
if(!defined($rmsg)) {
|
|
my $msg = "MAXLAN_ExpectAnswer: Error while waiting for answer $expectedanswer";
|
|
Log 1, $msg;
|
|
return $msg;
|
|
}
|
|
|
|
my $ret = undef;
|
|
if($rmsg !~ m/^$expectedanswer/) {
|
|
Log 2, "MAXLAN_ExpectAnswer: Got unexpected response, expected $expectedanswer";
|
|
MAXLAN_Parse($hash,$rmsg);
|
|
return "Got unexpected response, expected $expectedanswer";
|
|
}
|
|
MAXLAN_Parse($hash,$rmsg);
|
|
return undef;
|
|
}
|
|
|
|
|
|
#Reads single line from the Cube
|
|
#blocks if waitForResponse is true
|
|
#
|
|
#returns undef, if an error occured,
|
|
#otherwise the line
|
|
sub
|
|
MAXLAN_ReadSingleResponse($$)
|
|
{
|
|
my ($hash,$waitForResponse) = @_;
|
|
|
|
return undef if(!MAXLAN_IsConnected($hash));
|
|
|
|
my ($rin, $win, $ein, $rout, $wout, $eout);
|
|
$rin = $win = $ein = '';
|
|
vec($rin,fileno($hash->{TCPDev}),1) = 1;
|
|
$ein = $rin;
|
|
|
|
my $maxTime = gettimeofday()+$read_timeout;
|
|
|
|
#Read until we have a complete line
|
|
until($hash->{PARTIAL} =~ m/\n/) {
|
|
|
|
#Check timeout
|
|
if(gettimeofday() > $maxTime) {
|
|
if($waitForResponse) {
|
|
Log 1, "MAXLAN_ReadSingleResponse: timeout while reading from socket, disconnecting";
|
|
MAXLAN_Disconnect($hash);
|
|
}
|
|
return undef;;
|
|
}
|
|
|
|
#Wait for data
|
|
my $nfound = select($rout=$rin, $wout=$win, $eout=$ein, $read_timeout);
|
|
if($nfound == -1) {
|
|
Log 1, "MAXLAN_ReadSingleResponse: error during select, ret = $nfound";
|
|
return undef;
|
|
}
|
|
last if($nfound == 0 and !$waitForResponse);
|
|
next if($nfound == 0); #Sometimes select() returns early, just try again
|
|
|
|
#Blocking read
|
|
my $buf;
|
|
my $res = sysread($hash->{TCPDev}, $buf, 256);
|
|
if(!defined($res)){
|
|
Log 1, "MAXLAN_ReadSingleResponse: error during read";
|
|
return undef; #error occured
|
|
}
|
|
|
|
#Append data to partial data we got before
|
|
$hash->{PARTIAL} .= $buf;
|
|
}
|
|
|
|
my $rmsg;
|
|
($rmsg,$hash->{PARTIAL}) = split("\n", $hash->{PARTIAL}, 2);
|
|
$rmsg =~ s/\r//; #remove \r
|
|
return $rmsg;
|
|
}
|
|
|
|
my %lhash;
|
|
|
|
#####################################
|
|
#Sends given msg and checks for/parses the answer
|
|
#returns undef on success
|
|
sub
|
|
MAXLAN_Write(@)
|
|
{
|
|
my ($hash,$msg,$expectedAnswer) = @_;
|
|
my $ret = undef;
|
|
|
|
$ret = MAXLAN_Connect($hash); #It's a no-op if already connected
|
|
return "MAXLAN_Write: $ret" if($ret);
|
|
$ret = MAXLAN_SimpleWrite($hash, $msg);
|
|
return "MAXLAN_Write: $ret" if($ret);
|
|
if($expectedAnswer) {
|
|
$ret = MAXLAN_ExpectAnswer($hash, $expectedAnswer);
|
|
return "MAXLAN_Write: $ret" if($ret);
|
|
}
|
|
MAXLAN_Disconnect($hash) if(!$hash->{persistent} && !$hash->{pairmode});
|
|
return undef;
|
|
}
|
|
|
|
#####################################
|
|
# called from the global loop, when the select for hash->{FD} reports data
|
|
sub
|
|
MAXLAN_Read($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
while(1) {
|
|
my $rmsg = MAXLAN_ReadSingleResponse($hash, 0);
|
|
last if(!$rmsg);
|
|
# The Msg N: .... is the only one that may come spontanously from
|
|
# the cube while we are in pairmode
|
|
Log 2, "Unsolicated response from Cube: $rmsg" unless($hash->{pairmode} and substr($rmsg,0,2) eq "N:");
|
|
MAXLAN_Parse($hash, $rmsg);
|
|
}
|
|
}
|
|
|
|
sub
|
|
MAXLAN_SendMetadata($)
|
|
{
|
|
my $hash = shift;
|
|
|
|
if(defined($hash->{metadataVersionMismatch})){
|
|
Log 3,"MAXLAN_SendMetadata: current version of metadata unexpected, not overwriting!";
|
|
return;
|
|
}
|
|
|
|
my $maxNameLength = 32;
|
|
my $maxGroupCount = 20;
|
|
my $maxDeviceCount = 140;
|
|
|
|
my @groups = @{$hash->{groups}};
|
|
my @devices = @{$hash->{devices}};
|
|
|
|
if(@groups > $maxGroupCount || @devices > $maxDeviceCount) {
|
|
Log 1, "MAXLAN_SendMetadata: you got more than $maxGroupCount groups or $maxDeviceCount devices";
|
|
return;
|
|
}
|
|
|
|
my $metadata = pack("CC",$metadata_magic,$metadata_version);
|
|
|
|
$metadata .= pack("C",scalar(@groups));
|
|
foreach(@groups){
|
|
if(length($_->{name}) > $maxNameLength) {
|
|
Log 1, "Group name $_->{name} is too long, maximum of $maxNameLength characters allowed";
|
|
return;
|
|
}
|
|
$metadata .= pack("CC/aH6",$_->{id}, $_->{name}, $_->{masterAddr});
|
|
}
|
|
$metadata .= pack("C",scalar(@devices));
|
|
foreach(@devices){
|
|
if(length($_->{name}) > $maxNameLength) {
|
|
Log 1, "Device name $_->{name} is too long, maximum of $maxNameLength characters allowed";
|
|
return;
|
|
}
|
|
$metadata .= pack("CH6a[10]C/aC",$_->{type}, $_->{addr}, $_->{serial}, $_->{name}, $_->{groupid});
|
|
}
|
|
|
|
$metadata .= pack("C",1); #dstenables, should always be 1
|
|
my $blocksize = 1900;
|
|
|
|
$metadata = encode_base64($metadata,"");
|
|
|
|
my $numpackages = ceil(length($metadata)/$blocksize);
|
|
for(my $i=0;$i < $numpackages; $i++) {
|
|
my $package = substr($metadata,$i*$blocksize,$blocksize);
|
|
|
|
return MAXLAN_Write($hash,"m:".sprintf("%02d",$i).",".$package, "A:");
|
|
}
|
|
}
|
|
|
|
# Maps [9,61] -> [off,5.0,5.5,...,30.0,on]
|
|
sub
|
|
MAXLAN_ExtractTemperature($)
|
|
{
|
|
return $_[0] == 61 ? "on" : ($_[0] == 9 ? "off" : sprintf("%2.1f",$_[0]/2));
|
|
}
|
|
|
|
sub
|
|
MAXLAN_Parse($$)
|
|
{
|
|
#http://www.domoticaforum.eu/viewtopic.php?f=66&t=6654
|
|
my ($hash, $rmsg) = @_;
|
|
|
|
my $name = $hash->{NAME};
|
|
my $ll3 = GetLogLevel($name,3);
|
|
my $ll5 = GetLogLevel($name,5);
|
|
Log $ll5, "Msg $rmsg";
|
|
my $cmd = substr($rmsg,0,1); # get leading char
|
|
my @args = split(',', substr($rmsg,2));
|
|
#Log $ll5, 'args '.join(" ",@args);
|
|
|
|
if ($cmd eq 'H'){ #Hello
|
|
$hash->{serial} = $args[0];
|
|
$hash->{addr} = $args[1];
|
|
$modules{MAX}{defptr}{$hash->{addr}} = $hash;
|
|
$hash->{fwversion} = $args[2];
|
|
my $dutycycle = 0;
|
|
if(@args > 5){
|
|
$dutycycle = hex($args[5]);
|
|
$hash->{dutycycle} = sprintf("%3.0f %%", $dutycycle);
|
|
readingsSingleUpdate( $hash, 'dutycycle', $dutycycle, 1 );
|
|
}
|
|
my $freememory = 0;
|
|
if(@args > 6){
|
|
$freememory = $args[6];
|
|
}
|
|
my $cubedatetime = {
|
|
year => 2000+hex(substr($args[7],0,2)),
|
|
month => hex(substr($args[7],2,2)),
|
|
day => hex(substr($args[7],4,2)),
|
|
hour => hex(substr($args[8],0,2)),
|
|
minute => hex(substr($args[8],2,2)),
|
|
};
|
|
$hash->{clockset} = hex($args[9]);
|
|
#$cubedatetime field is only valid if $clockset is 1
|
|
if($hash->{clockset}) {
|
|
my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
|
|
my $difference = ((((($cubedatetime->{year} - $year-1900)*12
|
|
+ $cubedatetime->{month} - $mon-1)*30
|
|
+ $cubedatetime->{day} - $mday)*24
|
|
+ $cubedatetime->{hour} - $hour)*60
|
|
+ $cubedatetime->{minute} - $min);
|
|
$hash->{cubeTimeDifference} = $difference;
|
|
if($difference > 1) {
|
|
Log 2, "MAXLAN_Parse: Cube thinks it is $cubedatetime->{day}.$cubedatetime->{month}.$cubedatetime->{year} $cubedatetime->{hour}:$cubedatetime->{minute}";
|
|
Log 2, "MAXLAN_Parse: Time difference is $difference minutes";
|
|
}
|
|
} else {
|
|
Log 2, "MAXLAN_Parse: Cube has no time set";
|
|
}
|
|
|
|
Log $ll5, "MAXLAN_Parse: Got hello, connection ip $args[4], duty cycle $dutycycle, freememory $freememory, clockset $hash->{clockset}";
|
|
|
|
} elsif($cmd eq 'M') {
|
|
#Metadata, this is basically a readwrite part of the cube's memory.
|
|
#I don't think that the cube interprets any of that data.
|
|
#One can write to that memory with the "m:" command
|
|
#The actual configuration comes with the "C:" response and can be set
|
|
#with the "s:" command.
|
|
return $name if(@args < 3); #On virgin devices, we get nothing, not even $magic$version$numgroups$numdevices
|
|
|
|
my $bindata = decode_base64($args[2]);
|
|
#$version is the version the serialized data format I guess
|
|
my ($magic,$version,$numgroups,@groupsdevices);
|
|
eval {
|
|
($magic,$version,$numgroups,@groupsdevices) = unpack("CCCXC/(CC/aH6)C/(CH6a[10]C/aC)C",$bindata);
|
|
1;
|
|
} or do {
|
|
Log 1, "MAXLAN_Parse: Metadata response is malformed!";
|
|
return $name;
|
|
};
|
|
|
|
if($magic != $metadata_magic || $version != $metadata_version) {
|
|
Log 3, "MAXLAN_Parse: magic $magic/version $version are not $metadata_magic/$metadata_version as expected";
|
|
$hash->{metadataVersionMismatch} = 1;
|
|
}
|
|
|
|
my $daylightsaving = pop(@groupsdevices); #should be always true (=0x01)
|
|
|
|
my $i;
|
|
$hash->{groups} = ();
|
|
for($i=0;$i<3*$numgroups;$i+=3){
|
|
$hash->{groups}[@{$hash->{groups}}]->{id} = $groupsdevices[$i];
|
|
$hash->{groups}[-1]->{name} = $groupsdevices[$i+1];
|
|
$hash->{groups}[-1]->{masterAddr} = $groupsdevices[$i+2];
|
|
}
|
|
#After a device is freshly paired, it does not appear in this metadata response,
|
|
#we first have to set some metadata for it
|
|
$hash->{devices} = ();
|
|
for(;$i<@groupsdevices;$i+=5){
|
|
$hash->{devices}[@{$hash->{devices}}]->{type} = $groupsdevices[$i];
|
|
$hash->{devices}[-1]->{addr} = $groupsdevices[$i+1];
|
|
$hash->{devices}[-1]->{serial} = $groupsdevices[$i+2];
|
|
$hash->{devices}[-1]->{name} = $groupsdevices[$i+3];
|
|
$hash->{devices}[-1]->{groupid} = $groupsdevices[$i+4];
|
|
}
|
|
|
|
#Log $ll5, "Got Metadata, hash: ".Dumper($hash);
|
|
|
|
}elsif($cmd eq "C"){#Configuration
|
|
return $name if(@args < 2);
|
|
my $bindata = decode_base64($args[1]);
|
|
|
|
if(length($bindata) < 18) {
|
|
Log 1, "Invalid C: response, not enough data";
|
|
return $name;
|
|
}
|
|
|
|
#Parse the first 18 bytes, those are send for every device
|
|
my ($len,$addr,$devicetype,$groupid,$firmware,$testresult,$serial) = unpack("CH6CCCCa[10]", $bindata);
|
|
Log $ll5, "MAXLAN_Parse: len $len, addr $addr, devicetype $devicetype, firmware $firmware, testresult $testresult, groupid $groupid, serial $serial";
|
|
|
|
$len = $len+1; #The len field itself was not counted
|
|
|
|
Dispatch($hash, "MAX,1,define,$addr,$device_types{$devicetype},$serial,$groupid", {}) if($device_types{$devicetype} ne "Cube");
|
|
|
|
#Set firmware and testresult on device
|
|
my $dhash = $modules{MAX}{defptr}{$addr};
|
|
if(defined($dhash)) {
|
|
readingsBeginUpdate($dhash);
|
|
readingsBulkUpdate($dhash, "firmware", sprintf("%u.%u",int($firmware/16),$firmware%16));
|
|
readingsBulkUpdate($dhash, "testresult", $testresult);
|
|
readingsEndUpdate($dhash, 1);
|
|
}
|
|
|
|
if($len != length($bindata)) {
|
|
Dispatch($hash, "MAX,1,Error,$addr,Parts of configuration are missing", {});
|
|
return $name;
|
|
}
|
|
|
|
#devicetype: Cube = 0, HeatingThermostat = 1, HeatingThermostatPlus = 2, WallMountedThermostat = 3, ShutterContact = 4, PushButton = 5
|
|
#Seems that ShutterContact does not have any configdata
|
|
if($device_types{$devicetype} eq "Cube"){
|
|
#TODO: there is a lot of data left to interpret
|
|
|
|
}elsif($device_types{$devicetype} =~ /HeatingThermostat.*/){
|
|
my ($comforttemp,$ecotemp,$maxsetpointtemp,$minsetpointtemp,$tempoffset,$windowopentemp,$windowopendur,$boost,$decalcifiction,$maxvalvesetting,$valveoffset,$weekprofile) = unpack("CCCCCCCCCCCH364",substr($bindata,18));
|
|
my $boostValve = ($boost & 0x1F) * 5;
|
|
my $boostDuration = $boost >> 5;
|
|
$comforttemp = MAXLAN_ExtractTemperature($comforttemp); #convert to degree celcius
|
|
$ecotemp = MAXLAN_ExtractTemperature($ecotemp); #convert to degree celcius
|
|
$tempoffset = $tempoffset/2.0-3.5; #convert to degree
|
|
$maxsetpointtemp = MAXLAN_ExtractTemperature($maxsetpointtemp);
|
|
$minsetpointtemp = MAXLAN_ExtractTemperature($minsetpointtemp);
|
|
$windowopentemp = MAXLAN_ExtractTemperature($windowopentemp);
|
|
$windowopendur *= 5;
|
|
$maxvalvesetting = int($maxvalvesetting*100/255 + 0.5); # + 0.5 for correct rounding
|
|
$valveoffset = int($valveoffset*100/255 + 0.5); # + 0.5 for correct rounding
|
|
my $decalcDay = ($decalcifiction >> 5) & 0x07;
|
|
my $decalcTime = $decalcifiction & 0x1F;
|
|
Log $ll5, "comfortemp $comforttemp, ecotemp $ecotemp, boostValve $boostValve, boostDuration $boostDuration, tempoffset $tempoffset, minsetpointtemp $minsetpointtemp, maxsetpointtemp $maxsetpointtemp, windowopentemp $windowopentemp, windowopendur $windowopendur";
|
|
Dispatch($hash, "MAX,1,HeatingThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$boostValve,$boostDuration,$tempoffset,$windowopentemp,$windowopendur,$maxvalvesetting,$valveoffset,$decalcDay,$decalcTime", {});
|
|
|
|
}elsif($device_types{$devicetype} eq "WallMountedThermostat"){
|
|
my ($comforttemp,$ecotemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$tempoffset,$windowopentemp,$boost) = unpack("CCCCH364CCC",substr($bindata,18));
|
|
$comforttemp = MAXLAN_ExtractTemperature($comforttemp);
|
|
$ecotemp = MAXLAN_ExtractTemperature($ecotemp);
|
|
$maxsetpointtemp = MAXLAN_ExtractTemperature($maxsetpointtemp);
|
|
$minsetpointtemp = MAXLAN_ExtractTemperature($minsetpointtemp);
|
|
Log $ll5, "comfortemp $comforttemp, ecotemp $ecotemp, minsetpointtemp $minsetpointtemp, maxsetpointtemp $maxsetpointtemp";
|
|
if(defined($tempoffset)) { #With firmware 18 (opposed to firmware 16)
|
|
$tempoffset = $tempoffset/2.0-3.5; #convert to degree
|
|
my $boostValve = ($boost & 0x1F) * 5;
|
|
my $boostDuration = $boost >> 5;
|
|
$windowopentemp = MAXLAN_ExtractTemperature($windowopentemp);
|
|
Log $ll5, "tempoffset $tempoffset, boostValve $boostValve, boostDuration $boostDuration, windowOpenTemp $windowopentemp";
|
|
Dispatch($hash, "MAX,1,WallThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile,$boostValve,$boostDuration,$tempoffset,$windowopentemp", {});
|
|
} else {
|
|
Dispatch($hash, "MAX,1,WallThermostatConfig,$addr,$ecotemp,$comforttemp,$maxsetpointtemp,$minsetpointtemp,$weekprofile", {});
|
|
}
|
|
|
|
}elsif($device_types{$devicetype} eq "ShutterContact"){
|
|
Log 2, "MAXLAN_Parse: ShutterContact send some configuration, but none was expected" if($len > 18);
|
|
}elsif($device_types{$devicetype} eq "PushButton"){
|
|
Log 2, "MAXLAN_Parse: PushButton send some configuration, but none was expected" if($len > 18);
|
|
}else{ #TODO
|
|
Log 2, "MAXLAN_Parse: Got configdata for unimplemented devicetype $devicetype";
|
|
}
|
|
|
|
#Clear Error
|
|
Dispatch($hash, "MAX,1,Error,$addr", {}) if($addr ne $hash->{addr}); #don't clear own error
|
|
|
|
#Check if it is already recorded in devices
|
|
my $found = 0;
|
|
foreach (@{$hash->{devices}}) {
|
|
$found = 1 if($_->{addr} eq $addr);
|
|
}
|
|
#Add device if it is not already known and not the cube itself
|
|
if(!$found && $devicetype != 0){
|
|
$hash->{devices}[@{$hash->{devices}}]->{type} = $devicetype;
|
|
$hash->{devices}[-1]->{addr} = $addr;
|
|
$hash->{devices}[-1]->{serial} = $serial;
|
|
$hash->{devices}[-1]->{name} = "no name";
|
|
$hash->{devices}[-1]->{groupid} = $groupid;
|
|
}
|
|
|
|
}elsif($cmd eq 'L'){#List
|
|
|
|
my $bindata = "";
|
|
$bindata = decode_base64($args[0]) if(@args > 0);
|
|
#The L command consists of blocks of states (one for each device)
|
|
while(length($bindata)){
|
|
my ($len,$addr,$errCmd,$bits1) = unpack("CH6H2a",$bindata);
|
|
$errCmd = uc($errCmd);
|
|
my $unkbit1 = vec($bits1,0,1);
|
|
my $initialized = vec($bits1,1,1); #I never saw this beeing 0
|
|
my $answer = vec($bits1,2,1); #answer to what?
|
|
my $error = vec($bits1,3,1); # if 1 then see errframetype
|
|
my $valid = vec($bits1,4,1); #is the status following the common header valid
|
|
my $unkbit2 = vec($bits1,5,2);
|
|
my $unkbit3 = vec($bits1,7,1);
|
|
|
|
Log 5, "len $len, addr $addr, initialized $initialized, valid $valid, error $error, errCmd $errCmd, answer $answer, unkbit ($unkbit1,$unkbit2,$unkbit3)";
|
|
|
|
my $payload = unpack("H*",substr($bindata,6,$len-6+1)); #+1 because the len field is not counted
|
|
if($valid) {
|
|
my $shash = $modules{MAX}{defptr}{$addr};
|
|
|
|
if(!$shash) {
|
|
Log 2, "Got List response for undefined device with addr $addr";
|
|
}elsif($shash->{type} =~ /HeatingThermostat.*/){
|
|
Dispatch($hash, "MAX,1,ThermostatState,$addr,$payload", {});
|
|
}elsif($shash->{type} eq "WallMountedThermostat"){
|
|
Dispatch($hash, "MAX,1,WallThermostatState,$addr,$payload", {});
|
|
}elsif($shash->{type} eq "ShutterContact"){
|
|
Dispatch($hash, "MAX,1,ShutterContactState,$addr,$payload", {});
|
|
}elsif($shash->{type} eq "PushButton"){
|
|
Dispatch($hash, "MAX,1,PushButtonState,$addr,$payload", {});
|
|
}else{
|
|
Log 2, "MAXLAN_Parse: Got status for unimplemented device type $shash->{type}";
|
|
}
|
|
}
|
|
|
|
my $dhash = $modules{MAX}{defptr}{$addr};
|
|
if(defined($dhash)) {
|
|
readingsBeginUpdate($dhash);
|
|
readingsBulkUpdate($dhash, "MAXLAN_initialized", $initialized);
|
|
readingsBulkUpdate($dhash, "MAXLAN_error", $error);
|
|
readingsBulkUpdate($dhash, "MAXLAN_errorInCommand", $error ? (exists($msgId2Cmd{$errCmd}) ? $msgId2Cmd{$errCmd} : $errCmd) : "");
|
|
readingsBulkUpdate($dhash, "MAXLAN_valid", $valid);
|
|
readingsBulkUpdate($dhash, "MAXLAN_isAnswer", $answer);
|
|
readingsEndUpdate($dhash, 1);
|
|
if($error) {
|
|
MAXLAN_Write($hash,"r:01,".encode_base64(pack("H*",$addr),""), "S:");
|
|
}
|
|
}
|
|
|
|
$bindata=substr($bindata,$len+1); #+1 because the len field is not counted
|
|
} # while(length($bindata))
|
|
|
|
}elsif($cmd eq "N"){#New device paired
|
|
if(@args==0){
|
|
$hash->{STATE} = "initalized"; #pairing ended
|
|
$hash->{pairmode} = 0;
|
|
return $name;
|
|
}
|
|
my ($type, $addr, $serial) = unpack("CH6a[10]", decode_base64($args[0]));
|
|
Log 2, "MAXLAN_Parse: Paired new device, type $device_types{$type}, addr $addr, serial $serial";
|
|
Dispatch($hash, "MAX,1,define,$addr,$device_types{$type},$serial,0", {});
|
|
|
|
#After a device has been paired, it automatically appears in the "L" and "C" commands,
|
|
MAXLAN_RequestConfiguration($hash,$addr);
|
|
} elsif($cmd eq "A"){#Acknowledged
|
|
|
|
} elsif($cmd eq "S"){#Response to s:
|
|
$hash->{dutycycle} = hex($args[0]); #number of command send over the air
|
|
readingsSingleUpdate( $hash, 'dutycycle', $hash->{dutycycle}, 1 );
|
|
|
|
my $discarded = $args[1];
|
|
$hash->{freememoryslot} = hex($args[2]);
|
|
Log 5, "MAXLAN_Parse: dutycyle $hash->{dutycycle}, freememoryslot $hash->{freememoryslot}";
|
|
|
|
Log 3, "MAXLAN_Parse: 1% rule: we sent too much, cmd is now in queue" if($hash->{dutycycle} == 100 && $hash->{freememoryslot} > 0);
|
|
Log 2, "MAXLAN_Parse: 1% rule: we sent too much, queue is full" if($hash->{dutycycle} == 100 && $hash->{freememoryslot} == 0);
|
|
Log 2, "MAXLAN_Parse: Command was discarded" if($discarded);
|
|
} else {
|
|
Log 2, "MAXLAN_Parse: Unknown command $cmd";
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
|
|
########################
|
|
#Returns undef on sucess
|
|
sub
|
|
MAXLAN_SimpleWrite(@)
|
|
{
|
|
my ($hash, $msg) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
Log GetLogLevel($name,5), 'MAXLAN_SimpleWrite: '.$msg;
|
|
|
|
return "MAXLAN_SimpleWrite: Not connected" if(!MAXLAN_IsConnected($hash));
|
|
|
|
$msg .= "\r\n";
|
|
|
|
my $ret = syswrite($hash->{TCPDev}, $msg);
|
|
#TODO: none of those conditions detect if the connection is actually lost!
|
|
if(!$hash->{TCPDev} || !defined($ret) || !$hash->{TCPDev}->connected) {
|
|
Log GetLogLevel($name,1), 'MAXLAN_SimpleWrite failed';
|
|
MAXLAN_Disconnect($hash);
|
|
return "MAXLAN_SimpleWrite: syswrite failed";
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
########################
|
|
sub
|
|
MAXLAN_DoInit($)
|
|
{
|
|
my ($hash) = @_;
|
|
return undef;
|
|
}
|
|
|
|
#Returns undef on success
|
|
sub
|
|
MAXLAN_RequestList($)
|
|
{
|
|
my $hash = shift;
|
|
return MAXLAN_Write($hash, "l:", "L:");
|
|
}
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Poll($)
|
|
{
|
|
my $hash = shift;
|
|
|
|
my $ret = undef;
|
|
if(MAXLAN_IsConnected($hash)) {
|
|
$ret = MAXLAN_RequestList($hash);
|
|
} else {
|
|
#Connecting gives us a RequestList for free
|
|
$ret = MAXLAN_Connect($hash);
|
|
}
|
|
|
|
if($ret) {
|
|
#Connecting failed/Got invalid answer
|
|
MAXLAN_Disconnect($hash);
|
|
InternalTimer(gettimeofday()+$reconnect_interval, "MAXLAN_Poll", $hash, 0);
|
|
return;
|
|
}
|
|
|
|
MAXLAN_Disconnect($hash) if(!$hash->{persistent} && !$hash->{pairmode});
|
|
|
|
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MAXLAN_Poll", $hash, 0);
|
|
}
|
|
|
|
#This only works for a device that got just paired
|
|
sub
|
|
MAXLAN_RequestConfiguration($$)
|
|
{
|
|
my ($hash,$addr) = @_;
|
|
return MAXLAN_Write($hash,"c:$addr", "C:");
|
|
}
|
|
|
|
sub
|
|
MAXLAN_Send(@)
|
|
{
|
|
my ($hash, $cmd, $dst, $payload, %opts) = @_;
|
|
|
|
my $flags = "00";
|
|
my $groupId = "00";
|
|
my $callbackParam = undef;
|
|
|
|
$flags = $opts{flags} if(exists($opts{flags}));
|
|
$groupId = $opts{groupId} if(exists($opts{groupId}));
|
|
Log 2, "MAXLAN_Send: MAXLAN does not support src" if(exists($opts{src}));
|
|
$callbackParam = $opts{callbackParam} if(exists($opts{callbackParam}));
|
|
|
|
$payload = pack("H*","00".$flags.$msgCmd2Id{$cmd}."000000".$dst.$groupId.$payload);
|
|
|
|
my $ret = MAXLAN_Write($hash,"s:".encode_base64($payload,""), "S:");
|
|
#TODO: actually check return value
|
|
if(defined($opts{callbackParam})) {
|
|
Dispatch($hash, "MAX,1,Ack$cmd,$dst,$opts{callbackParam}", {});
|
|
}
|
|
#Reschedule a poll in the near future after the cube will
|
|
#have gotten an answer
|
|
RemoveInternalTimer($hash);
|
|
InternalTimer(gettimeofday()+$roundtriptime, "MAXLAN_Poll", $hash, 0);
|
|
return $ret;
|
|
}
|
|
|
|
#Resets the cube, i.e. do a factory reset. All pairings will be lost from the cube
|
|
#(but you will have to manually reset each individual device.
|
|
sub
|
|
MAXLAN_RequestReset($)
|
|
{
|
|
my $hash = shift;
|
|
return MAXLAN_Write($hash,"a:", "A:");
|
|
}
|
|
|
|
#Remove the device from the cube, i.e. deletes the pairing
|
|
sub
|
|
MAXLAN_RemoveDevice($$)
|
|
{
|
|
my ($hash,$addr) = @_;
|
|
#This does a factoryReset on the Device
|
|
my $ret = MAXLAN_Write($hash,"t:1,1,".encode_base64(pack("H6",$addr),""), "A:");
|
|
if(!defined($ret)) { #success
|
|
#The device is not longer accessable by the Cube
|
|
$modules{MAX}{defptr}{$addr}{IODev} = undef;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
1;
|
|
|
|
=pod
|
|
=begin html
|
|
|
|
<a name="MAXLAN"></a>
|
|
<h3>MAXLAN</h3>
|
|
<ul>
|
|
<tr><td>
|
|
The MAXLAN is the fhem module for the eQ-3 MAX! Cube LAN Gateway.
|
|
<br><br>
|
|
The fhem module makes the MAX! "bus" accessible to fhem, automatically detecting paired MAX! devices. It also represents properties of the MAX! Cube. The other devices are handled by the <a href="#MAX">MAX</a> module, which uses this module as its backend.<br>
|
|
<br>
|
|
|
|
<a name="MAXLANdefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> MAXLAN <ip-address>[:port] [<pollintervall> [ondemand]]</code><br>
|
|
<br>
|
|
port is 62910 by default. (If your Cube listens on port 80, you have to update the firmware with
|
|
the official MAX! software).
|
|
If the ip-address is called none, then no device will be opened, so you
|
|
can experiment without hardware attached.<br>
|
|
The optional parameter <pollintervall> defines the time in seconds between each polling of data from the cube.<br>
|
|
You may provide the option <code>ondemand</code> forcing the MAXLAN module to tear-down the connection as often as possible
|
|
thus making the cube usable by other applications or the web portal.
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="MAXLANset"></a>
|
|
<b>Set</b>
|
|
<ul>
|
|
<li>pairmode [<n>,cancel]<br>
|
|
Sets the cube into pairing mode for <n> seconds (default is 60s ) where it can be paired with other devices (Thermostats, Buttons, etc.). You also have to set the other device into pairing mode manually. (For Thermostats, this is pressing the "Boost" button for 3 seconds, for example).
|
|
Setting pairmode to "cancel" puts the cube out of pairing mode.</li>
|
|
<li>raw <data><br>
|
|
Sends the raw <data> to the cube.</li>
|
|
<li>clock<br>
|
|
Sets the internal clock in the cube to the current system time of fhem's machine (uses timezone attribute if set). You can add<br>
|
|
<code>attr ml set-clock-on-init</code><br>
|
|
to your fhem.cfg to do this automatically on startup.</li>
|
|
<li>factorReset<br>
|
|
Reset the cube to factory defaults.</li>
|
|
<li>reconnect<br>
|
|
FHEM will terminate the current connection to the cube and then reconnect. This allows
|
|
re-reading the configuration data from the cube, as it is only send after establishing a new connection.</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="MAXLANget"></a>
|
|
<b>Get</b>
|
|
<ul>
|
|
N/A
|
|
</ul>
|
|
<br>
|
|
<br>
|
|
|
|
<a name="MAXLANattr"></a>
|
|
<b>Attributes</b>
|
|
<ul>
|
|
<li>set-clock-on-init<br>
|
|
(Default: 1). Automatically call "set clock" after connecting to the cube.</li>
|
|
<li><a href="#do_not_notify">do_not_notify</a></li>
|
|
<li><a href="#attrdummy">dummy</a></li>
|
|
<li><a href="#loglevel">loglevel</a></li>
|
|
<li><a href="#addvaltrigger">addvaltrigger</a></li>
|
|
<li>timezone<br>
|
|
(Default: CET-CEST). Set MAX Cube timezone (requires "set clock" to take effect).<br>
|
|
<b>NB.</b>Cube time and cubeTimeDifference will not change until Cube next connects.<br>
|
|
<ul>
|
|
<li>GMT-BST - (UTC +0, UTC+1)</li>
|
|
<li>CET-CEST - (UTC +1, UTC+2)</li>
|
|
<li>EET-EEST - (UTC +2, UTC+3)</li>
|
|
<li>FET-FEST - (UTC +3)</li>
|
|
<li>MSK-MSD - (UTC +4)</li>
|
|
</ul>
|
|
The following are settings with no DST (daylight saving time)
|
|
<ul>
|
|
<li>GMT - (UTC +0)</li>
|
|
<li>CET - (UTC +1)</li>
|
|
<li>EET - (UTC +2)</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|