mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-04-08 13:24:56 +00:00
707 lines
22 KiB
Perl
Executable File
707 lines
22 KiB
Perl
Executable File
##############################################
|
|
# $Id$
|
|
# Written by Matthias Gehre, M.Gehre@gmx.de, 2012
|
|
package main;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use MIME::Base64;
|
|
use Data::Dumper;
|
|
use POSIX;
|
|
|
|
sub MAXLAN_Parse($$);
|
|
sub MAXLAN_Read($);
|
|
sub MAXLAN_Write($$);
|
|
sub MAXLAN_ReadAnswer($);
|
|
sub MAXLAN_SimpleWrite(@);
|
|
sub MAXLAN_Poll($);
|
|
sub MAXLAN_SendDeviceCmd($$);
|
|
sub MAXLAN_RequestConfiguration($$);
|
|
sub MAXLAN_RemoveDevice($$);
|
|
|
|
my %device_types = (
|
|
0 => "Cube",
|
|
1 => "HeatingThermostat",
|
|
2 => "HeatingThermostatPlus",
|
|
3 => "WallMountedThermostat",
|
|
4 => "ShutterContact",
|
|
5 => "PushButton"
|
|
);
|
|
|
|
my @boost_durations = (0, 5, 10, 15, 20, 25, 30, 60);
|
|
|
|
#Time after which we reconnect after a failed connection attempt
|
|
my $reconnect_interval = 5; #seconds
|
|
|
|
#the time it takes after sending one command till we see its effect in the L: response
|
|
my $roundtriptime = 3; #seconds
|
|
|
|
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->{WriteFn} = "MAXLAN_Write";
|
|
$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 " .
|
|
"loglevel:0,1,2,3,4,5,6 addvaltrigger ";
|
|
}
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Define($$)
|
|
{
|
|
my ($hash, $def) = @_;
|
|
my @a = split("[ \t][ \t]*", $def);
|
|
|
|
if(@a < 3 or @a > 4) {
|
|
my $msg = "wrong syntax: define <name> MAXLAN ip[:port] [pollintervall]";
|
|
Log 2, $msg;
|
|
return $msg;
|
|
}
|
|
DevIo_CloseDev($hash);
|
|
|
|
my $name = $a[0];
|
|
my $dev = $a[2];
|
|
$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->{PARTIAL} = "";
|
|
$hash->{DeviceName} = $dev;
|
|
$hash->{INTERVAL} = @a > 3 ? $a[3] : $defaultPollInterval;
|
|
#This interface is shared with 14_CUL_MAX.pm
|
|
$hash->{SendDeviceCmd} = \&MAXLAN_SendDeviceCmd;
|
|
$hash->{RemoveDevice} = \&MAXLAN_RemoveDevice;
|
|
|
|
|
|
MAXLAN_Connect($hash);
|
|
}
|
|
|
|
sub
|
|
MAXLAN_Connect($)
|
|
{
|
|
my $hash = shift;
|
|
|
|
delete($hash->{NEXT_OPEN}); #work around the connection rate limiter in DevIo
|
|
my $ret = DevIo_OpenDev($hash, 0, "MAXLAN_DoInit");
|
|
if($hash->{STATE} ne "opened"){
|
|
Log 3, "Scheduling reconnect attempt in $reconnect_interval seconds";
|
|
InternalTimer(gettimeofday()+$reconnect_interval, "MAXLAN_Connect", $hash, 0);
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Undef($$)
|
|
{
|
|
my ($hash, $arg) = @_;
|
|
RemoveInternalTimer($hash);
|
|
MAXLAN_Write($hash,"q:");
|
|
DevIo_CloseDev($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:");
|
|
return MAXLAN_ExpectAnswer($hash,"N:");
|
|
} else {
|
|
my $duration = 60;
|
|
$duration = $args[0] if(@args > 0);
|
|
MAXLAN_Write($hash,"n:".sprintf("%04x",$duration));
|
|
$hash->{STATE} = "pairing";
|
|
}
|
|
|
|
}elsif($setting eq "raw"){
|
|
MAXLAN_Write($hash,$args[0]);
|
|
|
|
}elsif($setting eq "clock"){
|
|
if(!exists($hash->{rfaddr})){
|
|
Log 5, "Defering the setting of time until after hello";
|
|
$hash->{setTimeOnHello} = 1;
|
|
return;
|
|
}
|
|
|
|
#This encodes the winter/summer timezones, its meaning is not entirely clear
|
|
my $timezones = "Q0VUAAAKAAMAAA4QQ0VTVAADAAIAABwg";
|
|
|
|
#The offset was obtained by experiment and is up to 1 minute, I don't know exactly what
|
|
#time format the cube uses. Something based on ntp I guess. Maybe this only works in GMT+1?
|
|
my $time = time()-946684774;
|
|
my $rmsg = "v:".$timezones.",".sprintf("%08x",$time);
|
|
MAXLAN_Write($hash,$rmsg);
|
|
my $answer = MAXLAN_ReadAnswer($hash);
|
|
if($answer ne "A:"){
|
|
Log 1, "Failed to set clock, answer was $answer, expected A:";
|
|
}else{
|
|
Dispatch($hash, "MAX,CubeClockState,$hash->{rfaddr},1", {RAWMSG => $rmsg});
|
|
}
|
|
|
|
}elsif($setting eq "factoryReset") {
|
|
MAXLAN_RequestReset($hash);
|
|
}else{
|
|
return "Unknown argument $setting, choose one of pairmode raw clock factoryReset";
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
MAXLAN_ExpectAnswer($$)
|
|
{
|
|
my ($hash,$expectedanswer) = @_;
|
|
my $rmsg = MAXLAN_ReadAnswer($hash);
|
|
return "Error while receiving" if(!defined($rmsg)); #error is already logged in MAXLAN_ReadAnswer
|
|
|
|
my $ret = undef;
|
|
if($rmsg !~ m/^$expectedanswer/) {
|
|
Log 2, "MAXLAN_ParseAnswer: Got unexpected response, expected $expectedanswer";
|
|
MAXLAN_Parse($hash,$rmsg);
|
|
return "Got unexpected response, expected $expectedanswer";
|
|
}
|
|
return MAXLAN_Parse($hash,$rmsg);
|
|
}
|
|
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_ReadAnswer($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
my $data = $hash->{PARTIAL};
|
|
|
|
#Read until we have a complete line
|
|
until($data =~ m/\n/) {
|
|
my $buf = DevIo_SimpleRead($hash);
|
|
if(!defined($buf)){
|
|
Log 1, "MAXLAN_ReadAnswer: error during read";
|
|
return undef; #error occured
|
|
}
|
|
$data .= $buf;
|
|
}
|
|
|
|
my $rmsg;
|
|
($rmsg,$data) = split("\n", $data, 2);
|
|
$rmsg =~ s/\r//; #remove \r
|
|
#save partial data for next read
|
|
$hash->{PARTIAL} = $data;
|
|
return $rmsg;
|
|
}
|
|
|
|
my %lhash;
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Write($$)
|
|
{
|
|
my ($hash,$msg) = @_;
|
|
|
|
MAXLAN_SimpleWrite($hash, $msg);
|
|
}
|
|
|
|
#####################################
|
|
# called from the global loop, when the select for hash->{FD} reports data
|
|
sub
|
|
MAXLAN_Read($)
|
|
{
|
|
my ($hash) = @_;
|
|
|
|
my $buf = DevIo_SimpleRead($hash);
|
|
return "" if(!defined($buf));
|
|
my $name = $hash->{NAME};
|
|
|
|
my $data = $hash->{PARTIAL};
|
|
$data .= $buf;
|
|
|
|
while($data =~ m/\n/) {
|
|
my $rmsg;
|
|
($rmsg,$data) = split("\n", $data, 2);
|
|
$hash->{PARTIAL} = $data;
|
|
$rmsg =~ s/\r//;
|
|
MAXLAN_Parse($hash, $rmsg) if($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);
|
|
|
|
MAXLAN_Write($hash,"m:".sprintf("%02d",$i).",".$package);
|
|
my $answer = MAXLAN_ReadAnswer($hash);
|
|
if($answer ne "A:"){
|
|
Log 1, "SendMetadata got response $answer, expected 'A:'";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
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->{rfaddr} = $args[1];
|
|
$hash->{fwversion} = $args[2];
|
|
my $dutycycle = 0;
|
|
if(@args > 5){
|
|
$dutycycle = $args[5];
|
|
}
|
|
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)),
|
|
};
|
|
my $clockset = hex($args[9]);
|
|
#$cubedatetime is only valid if $clockset is 1
|
|
if($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);
|
|
Log 3, "Cube thinks it is $cubedatetime->{day}.$cubedatetime->{month}.$cubedatetime->{year} $cubedatetime->{hour}:$cubedatetime->{minute}";
|
|
Log 3, "Time difference is $difference minutes";
|
|
}
|
|
|
|
Dispatch($hash, "MAX,define,$hash->{rfaddr},Cube,$hash->{serial}", {RAWMSG => $rmsg});
|
|
Dispatch($hash, "MAX,CubeConnectionState,$hash->{rfaddr},1", {RAWMSG => $rmsg});
|
|
Dispatch($hash, "MAX,CubeClockState,$hash->{rfaddr},$clockset", {RAWMSG => $rmsg});
|
|
Log $ll5, "MAXLAN_Parse: Got hello, connection ip $args[4], duty cycle $dutycycle, freememory $freememory, clockset $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 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, "Metadata response is malformed!";
|
|
return;
|
|
};
|
|
|
|
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];
|
|
Dispatch($hash, "MAX,define,$hash->{devices}[-1]->{addr},$device_types{$hash->{devices}[-1]->{type}},$hash->{devices}[-1]->{serial},$hash->{devices}[-1]->{groupid}", {RAWMSG => $rmsg});
|
|
}
|
|
|
|
Log $ll5, "Got Metadata, hash: ".Dumper($hash);
|
|
|
|
}elsif($cmd eq "C"){#Configuration
|
|
return if(@args < 2);
|
|
my $bindata = decode_base64($args[1]);
|
|
|
|
if(length($bindata) < 18) {
|
|
Log 1, "Invalid C: response, not enough data";
|
|
return "Invalid C: response, not enough data";
|
|
}
|
|
|
|
#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, "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,define,$addr,$device_types{$devicetype},$serial,$groupid", {RAWMSG => $rmsg});
|
|
|
|
if($len != length($bindata)) {
|
|
Log 1, "Invalid C: response, len does not match";
|
|
return "Invalid C: response, len does not match";
|
|
}
|
|
|
|
#devicetype: Cube = 0, HeatingThermostat = 1, HeatingThermostatPlus = 2, WallMountedThermostat = 3, ShutterContact = 4, PushButton = 5
|
|
#Seems that ShutterContact does not have any configdata
|
|
if($devicetype == 0){#Cube
|
|
#TODO: there is a lot of data left to interpret
|
|
}elsif($devicetype == 1){#HeatingThermostat
|
|
my ($comforttemp,$ecotemp,$maxsetpointtemp,$minsetpointtemp,$tempoffset,$windowopentemp,$windowopendur,$boost,$decalcifiction,$maxvalvesetting,$valveoffset) = unpack("CCCCCCCCCCC",substr($bindata,18));
|
|
my $boostValve = ($boost & 0x1F) * 5;
|
|
my $boostDuration = $boost_durations[$boost >> 5]; #in minutes
|
|
#There is some trailing data missing, which maps to the weekly program
|
|
$comforttemp=$comforttemp/2.0; #convert to degree celcius
|
|
$ecotemp=$ecotemp/2.0; #convert to degree celcius
|
|
$tempoffset = $tempoffset/2.0-3.5; #convert to degree
|
|
$maxsetpointtemp=$maxsetpointtemp/2.0;
|
|
$minsetpointtemp=$minsetpointtemp/2.0;
|
|
$windowopentemp=$windowopentemp/2.0;
|
|
$windowopendur=$windowopendur*5;
|
|
Log $ll5, "comfortemp $comforttemp, ecotemp $ecotemp, boostValve $boostValve, boostDuration $boostDuration, tempoffset $tempoffset, $minsetpointtemp minsetpointtemp, maxsetpointtemp $maxsetpointtemp, windowopentemp $windowopentemp, windowopendur $windowopendur";
|
|
Dispatch($hash, "MAX,HeatingThermostatConfig,$addr,$ecotemp,$comforttemp,$boostValve,$boostDuration,$tempoffset,$maxsetpointtemp,$minsetpointtemp,$windowopentemp,$windowopendur", {RAWMSG => $rmsg});
|
|
}elsif($devicetype == 4){#ShutterContact TODO
|
|
Log 2, "ShutterContact send some configuration, but none was expected" if($len > 18);
|
|
}else{ #TODO
|
|
Log 2, "Got configdata for unimplemented devicetype $devicetype";
|
|
}
|
|
|
|
#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,$errframetype,$bits1) = unpack("CH6Ca",$bindata);
|
|
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 $rferror1 = 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,1);
|
|
my $unkbit3 = vec($bits1,6,2);
|
|
|
|
Log 5, "len $len, addr $addr, initialized $initialized, valid $valid, rferror $rferror1, errframetype $errframetype, 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} eq "HeatingThermostat"){
|
|
Dispatch($hash, "MAX,HeatingThermostatState,$addr,$payload", {RAWMSG => $rmsg});
|
|
}elsif($shash->{type} eq "ShutterContact"){
|
|
Dispatch($hash, "MAX,ShutterContactState,$addr,$payload", {RAWMSG => $rmsg});
|
|
}else{
|
|
Log 2, "Got status for unimplemented device type $shash->{type}";
|
|
}
|
|
} # if($valid)
|
|
$bindata=substr($bindata,$len+1); #+1 because the len field is not counted
|
|
} # while(length($bindata))
|
|
|
|
if(!$hash->{gothello}) {
|
|
# "L:..." is the last response after connection before the cube starts to idle
|
|
$hash->{gothello} = 1;
|
|
#Handle deferred setting of time (L: is the last response after connection before the cube starts to idle)
|
|
if(defined($hash->{setTimeOnHello})) {
|
|
MAXLAN_Set($hash,$hash->{NAME},"clock");
|
|
delete $hash->{setTimeOnHello};
|
|
}
|
|
#Enable polling timer
|
|
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MAXLAN_Poll", $hash, 0)
|
|
}
|
|
}elsif($cmd eq "N"){#New device paired
|
|
if(@args==0){
|
|
$hash->{STATE} = "initalized"; #pairing ended
|
|
return;
|
|
}
|
|
my ($type, $addr, $serial) = unpack("CH6a[10]", decode_base64($args[0]));
|
|
Log 2, "Paired new device, type $device_types{$type}, addr $addr, serial $serial";
|
|
Dispatch($hash, "MAX,define,$addr,$device_types{$type},$serial", {RAWMSG => $rmsg});
|
|
|
|
#After a device has been paired, it automatically appears in the "L" and "C" commands,
|
|
MAXLAN_RequestConfiguration($hash,$addr);
|
|
} elsif($cmd eq "A"){#Acknowledged
|
|
Log 3, "Got stray Acknowledged from cube, this should be read by MAXLAN_ReadAnswer";
|
|
|
|
} elsif($cmd eq "S"){#Response to s:
|
|
my $dutycycle = hex($args[0]); #number of command send over the air
|
|
my $discarded = $args[1];
|
|
my $freememoryslot = $args[2];
|
|
Log 5, "dutycyle $dutycycle, freememoryslot $freememoryslot";
|
|
|
|
Log 3, "1% rule: we sent too much, cmd is now in queue" if($dutycycle == 100 && $freememoryslot > 0);
|
|
Log 3, "1% rule: we sent too much, queue is full, cmd discarded" if($dutycycle == 100 && $freememoryslot == 0);
|
|
Log 3, "Command was discarded" if($discarded);
|
|
return "Command was discarded" if($discarded);
|
|
} else {
|
|
Log $ll5, "$name Unknown command $cmd";
|
|
return "Unknown command $cmd";
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
|
|
########################
|
|
sub
|
|
MAXLAN_SimpleWrite(@)
|
|
{
|
|
my ($hash, $msg) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
Log GetLogLevel($name,5), 'MAXLAN_SimpleWrite: '.$msg;
|
|
|
|
$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';
|
|
DevIo_CloseDev($hash);
|
|
RemoveInternalTimer($hash);
|
|
MAXLAN_Connect($hash);
|
|
}
|
|
}
|
|
|
|
########################
|
|
sub
|
|
MAXLAN_DoInit($)
|
|
{
|
|
my ($hash) = @_;
|
|
$hash->{gothello} = 0;
|
|
return undef;
|
|
}
|
|
|
|
sub
|
|
MAXLAN_RequestList($)
|
|
{
|
|
my $hash = shift;
|
|
MAXLAN_Write($hash, "l:");
|
|
return MAXLAN_ExpectAnswer($hash, "L:");
|
|
}
|
|
|
|
#####################################
|
|
sub
|
|
MAXLAN_Poll($)
|
|
{
|
|
my $hash = shift;
|
|
|
|
return if(!$hash->{FD});
|
|
|
|
if(!defined(MAXLAN_RequestList($hash))) {
|
|
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MAXLAN_Poll", $hash, 0);
|
|
} else {
|
|
Log 1, "MAXLAN_Poll: Did not get any answer";
|
|
}
|
|
}
|
|
|
|
sub
|
|
MAXLAN_RequestConfiguration($$)
|
|
{
|
|
my ($hash,$addr) = @_;
|
|
MAXLAN_Write($hash,"c:$addr");
|
|
MAXLAN_ExpectAnswer($hash, "C:");
|
|
}
|
|
|
|
#Sends command to a device and waits for acknowledgment
|
|
sub
|
|
MAXLAN_SendDeviceCmd($$)
|
|
{
|
|
my ($hash,$payload) = @_;
|
|
MAXLAN_Write($hash,"s:".encode_base64($payload,""));
|
|
RemoveInternalTimer($hash);
|
|
InternalTimer(gettimeofday()+$roundtriptime, "MAXLAN_Poll", $hash, 0);
|
|
return MAXLAN_ExpectAnswer($hash, "S:");
|
|
}
|
|
|
|
#Resets the cube, i.e. does a factory reset. All pairings will be lost.
|
|
sub
|
|
MAXLAN_RequestReset($)
|
|
{
|
|
my $hash = shift;
|
|
MAXLAN_Write($hash,"a:");
|
|
MAXLAN_ExpectAnswer($hash, "A:");
|
|
}
|
|
|
|
#Remove the device from the cube, i.e. deletes the pairing
|
|
sub
|
|
MAXLAN_RemoveDevice($$)
|
|
{
|
|
my ($hash,$addr) = @_;
|
|
MAXLAN_Write($hash,"t:1,1,".encode_base64(pack("H6",$addr),""));
|
|
MAXLAN_ExpectAnswer($hash, "A:");
|
|
}
|
|
|
|
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. (The devices themselves are handled by the <a href="#MAX">MAX</a> module).<br>
|
|
The MAXLAN module keeps a persistant connection to the cube. The cube only allows one connection at a time, so neither the Max! Software or the
|
|
Max! internet portal can be used at the same time.
|
|
<br>
|
|
|
|
<a name="MAXLANdefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> MAXLAN <ip-address>[:port] [<pollintervall>]</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>
|
|
</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. You can add<br>
|
|
<code>set ml clock</code><br>
|
|
to your fhem.cfg to do this automatically on startup.</li>
|
|
<li>factorReset<br>
|
|
Reset the cube to factory defaults.</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><a href="#do_not_notify">do_not_notify</a></li><br>
|
|
<li><a href="#attrdummy">dummy</a></li><br>
|
|
<li><a href="#loglevel">loglevel</a></li><br>
|
|
<li><a href="#addvaltrigger">addvaltrigger</a></li><br>
|
|
</ul>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|