############################################## # $Id$ # Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013 # # TODO: # - Send RemoveGroupId packet if groupid is set to 0 package main; use strict; use warnings; use MIME::Base64; use MaxCommon; sub MAX_Define($$); sub MAX_Undef($$); sub MAX_Initialize($); sub MAX_Parse($$); sub MAX_Set($@); sub MAX_MD15Cmd($$$); sub MAX_DateTime2Internal($); my @ctrl_modes = ( "auto", "manual", "temporary", "boost" ); my %boost_durations = (0 => 0, 1 => 5, 2 => 10, 3 => 15, 4 => 20, 5 => 25, 6 => 30, 7 => 60); my %boost_durationsInv = reverse %boost_durations; my %decalcDays = (0 => "Sat", 1 => "Sun", 2 => "Mon", 3 => "Tue", 4 => "Wed", 5 => "Thu", 6 => "Fri"); my %decalcDaysInv = reverse %decalcDays; sub validTemperature { return $_[0] eq "on" || $_[0] eq "off" || ($_[0] ~~ /^\d+(\.[05])?$/ && $_[0] >= 5 && $_[0] <= 30); } sub validWindowOpenDuration { return $_[0] ~~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 60; } sub validMeasurementOffset { return $_[0] ~~ /^-?\d+(\.[05])?$/ && $_[0] >= -3.5 && $_[0] <= 3.5; } sub validBoostDuration { return $_[0] ~~ /^\d+$/ && exists($boost_durationsInv{$_[0]}); } sub validValveposition { return $_[0] ~~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 100; } sub validDecalcification { my ($decalcDay, $decalcHour) = ($_[0] =~ /^(...) (\d{1,2}):00$/); return defined($decalcDay) && defined($decalcHour) && exists($decalcDaysInv{$decalcDay}) && 0 <= $decalcHour && $decalcHour < 24; } sub validWeekProfile { return length($_[0]) == 4*13*7; } my %readingDef = ( #min/max/default "maximumTemperature" => [ \&validTemperature, "on"], "minimumTemperature" => [ \&validTemperature, "off"], "comfortTemperature" => [ \&validTemperature, 21], "ecoTemperature" => [ \&validTemperature, 17], "windowOpenTemperature" => [ \&validTemperature, 12], "windowOpenDuration" => [ \&validWindowOpenDuration, 15], "measurementOffset" => [ \&validMeasurementOffset, 0], "boostDuration" => [ \&validBoostDuration, 5 ], "boostValveposition" => [ \&validValveposition, 80 ], "decalcification" => [ \&validDecalcification, "Sat 12:00" ], "maxValveSetting" => [ \&validValveposition, 100 ], "valveOffset" => [ \&validValveposition, 00 ], ".weekProfile" => [ \&validWeekProfile, $defaultWeekProfile ], ); my %interfaces = ( "Cube" => undef, "HeatingThermostat" => "thermostat;battery;temperature", "HeatingThermostatPlus" => "thermostat;battery;temperature", "WallMountedThermostat" => "thermostat;temperature;battery", "ShutterContact" => "switch_active;battery", "PushButton" => "switch_passive;battery" ); sub MAX_Initialize($) { my ($hash) = @_; Log GetLogLevel($hash->{NAME}, 5), "Calling MAX_Initialize"; $hash->{Match} = "^MAX"; $hash->{DefFn} = "MAX_Define"; $hash->{UndefFn} = "MAX_Undef"; $hash->{ParseFn} = "MAX_Parse"; $hash->{SetFn} = "MAX_Set"; $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 dummy:0,1 " . "showtime:1,0 loglevel:0,1,2,3,4,5,6 ". $readingFnAttributes; return undef; } ############################# sub MAX_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $hash->{NAME}; return "name \"$name\" is reserved for internal use" if($name eq "fakeWallThermostat" or $name eq "fakeShutterContact"); return "wrong syntax: define MAX addr" if(int(@a)!=4 || $a[3] !~ m/^[A-F0-9]{6}$/i); my $type = $a[2]; my $addr = lc($a[3]); #all addr should be lowercase if(exists($modules{MAX}{defptr}{$addr})) { my $msg = "MAX_Define: Device with addr $addr is already defined"; Log 1, $msg; return $msg; } Log GetLogLevel($hash->{NAME}, 5), "Max_define $type with addr $addr "; $hash->{type} = $type; $hash->{addr} = $addr; $modules{MAX}{defptr}{$addr} = $hash; $hash->{internals}{interfaces} = $interfaces{$type}; AssignIoPort($hash); return undef; } sub MAX_Undef($$) { my ($hash,$name) = @_; delete($modules{MAX}{defptr}{$hash->{addr}}); } sub MAX_DateTime2Internal($) { my($day, $month, $year, $hour, $min) = ($_[0] =~ /^(\d{2}).(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); return (($month&0xE) << 20) | ($day << 16) | (($month&1) << 15) | (($year-2000) << 8) | ($hour*2 + int($min/30)); } sub MAX_TypeToTypeId($) { foreach (keys %device_types) { return $_ if($_[0] eq $device_types{$_}); } Log 1, "MAX_TypeToTypeId: Invalid type $_[0]"; return 0; } sub MAX_CheckIODev($) { my $hash = shift; return !defined($hash->{IODev}) || ($hash->{IODev}{TYPE} ne "MAXLAN" && $hash->{IODev}{TYPE} ne "CUL_MAX"); } #Idenitify for numeric values and maps "on" and "off" to their temperatures sub MAX_ParseTemperature($) { return $_[0] eq "on" ? 30.5 : ($_[0] eq "off" ? 4.5 :$_[0]); } sub MAX_Validate(@) { my ($name,$val) = @_; return 1 if(!exists($readingDef{$name})); return $readingDef{$name}[0]->($val); } #Get a reading, validating it's current value (maybe forcing to the default if invalid) #"on" and "off" are converted to their numeric values sub MAX_ReadingsVal(@) { my ($hash,$name) = @_; my $val = MAX_ParseTemperature(ReadingsVal($hash->{NAME},$name,"")); #$readingDef{$name} array is [validatingFunc, defaultValue] if(exists($readingDef{$name}) and !$readingDef{$name}[0]->($val)) { #Error: invalid value Log 2, "MAX: Invalid value $val for READING $name. Forcing to $readingDef{$name}[1]"; $val = $readingDef{$name}[1]; #Save default value to READINGS if(exists($hash->{".updateTimestamp"})) { readingsBulkUpdate($hash,$name,$val); } else { readingsSingleUpdate($hash,$name,$val,0); } } return $val; } sub MAX_ParseWeekProfile(@) { my ($hash ) = @_; # Format of weekprofile: 16 bit integer (high byte first) for every control point, 13 control points for every day # each 16 bit integer value is parsed as # int time = (value & 0x1FF) * 5; # int hour = (time / 60) % 24; # int minute = time % 60; # int temperature = ((value >> 9) & 0x3F) / 2; my $beginUpdate = !exists($hash->{".updateTimestamp"}); readingsBeginUpdate($hash) if($beginUpdate); my $curWeekProfile = MAX_ReadingsVal($hash, ".weekProfile"); #parse weekprofiles for each day for (my $i=0;$i<7;$i++) { my (@time_prof, @temp_prof); for(my $j=0;$j<13;$j++) { $time_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))& 0x1FF) * 5; $temp_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))>> 9 & 0x3F ) / 2; } my @hours; my @minutes; my $j; for($j=0;$j<13;$j++) { $hours[$j] = ($time_prof[$j] / 60 % 24); $minutes[$j] = ($time_prof[$j]%60); #if 00:00 reached, last point in profile was found last if(int($hours[$j])==0 && int($minutes[$j])==0 ); } my $time_prof_str = "00:00"; my $temp_prof_str; for (my $k=0;$k<=$j;$k++) { $time_prof_str .= sprintf("-%02d:%02d", $hours[$k], $minutes[$k]); $temp_prof_str .= sprintf("%2.1f °C",$temp_prof[$k]); if ($k < $j) { $time_prof_str .= " / " . sprintf("%02d:%02d", $hours[$k], $minutes[$k]); $temp_prof_str .= " / "; } } readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-time", $time_prof_str ); readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-temp", $temp_prof_str ); } readingsEndUpdate($hash, 1) if($beginUpdate); } ############################# sub MAX_Set($@) { my ($hash, $devname, @a) = @_; my ($setting, @args) = @a; return "Invalid IODev" if(MAX_CheckIODev($hash)); if($setting eq "desiredTemperature" and $hash->{type} =~ /.*Thermostat.*/) { return "missing a value" if(@args == 0); my $temperature; my $until = undef; my $ctrlmode = 1; #0=auto, 1=manual; 2=temporary if($args[0] eq "auto") { #This enables the automatic/schedule mode where the thermostat follows the weekly program $temperature = @args > 1 ? MAX_ParseTemperature($args[1]) : 0; $ctrlmode = 0; #auto } elsif($args[0] eq "boost") { $temperature = 0; $ctrlmode = 3; #TODO: auto mode with temperature is also possible } elsif($args[0] eq "eco") { $temperature = MAX_ReadingsVal($hash,"ecoTemperature"); } elsif($args[0] eq "comfort") { $temperature = MAX_ReadingsVal($hash,"comfortTemperature"); }else{ $temperature = MAX_ParseTemperature($args[0]); } if(@args > 1 and ($args[1] eq "until") and ($ctrlmode == 1)) { $ctrlmode = 2; #temporary $until = sprintf("%06x",MAX_DateTime2Internal($args[2]." ".$args[3])); } my $payload = sprintf("%02x",int($temperature*2.0) | ($ctrlmode << 6)); $payload .= $until if(defined($until)); return ($hash->{IODev}{Send})->($hash->{IODev},"SetTemperature",$hash->{addr},$payload); }elsif($setting ~~ ["boostDuration", "boostValveposition", "decalcification","maxValveSetting","valveOffset"] and $hash->{type} =~ /HeatingThermostat.*/){ my $val = join(" ",@args); #decalcification contains a space if(!MAX_Validate($setting, $val)) { my $msg = "Invalid value $args[0] for $setting"; Log 1, $msg; return $msg; } my $boostDuration = MAX_ReadingsVal($hash,"boostDuration"); my $boostValveposition = MAX_ReadingsVal($hash,"boostValveposition"); my $decalcification = MAX_ReadingsVal($hash,"decalcification"); my $maxValveSetting = MAX_ReadingsVal($hash,"maxValveSetting"); my $valveOffset = MAX_ReadingsVal($hash,"valveOffset"); eval "\$$setting = $val"; my ($decalcDay, $decalcHour) = ($decalcification =~ /^(...) (\d{1,2}):00$/); my $decalc = ($decalcDaysInv{$decalcDay} << 5) | $decalcHour; my $boost = ($boost_durationsInv{$boostDuration} << 5) | int($boostValveposition/5); my $payload = sprintf("%02x%02x%02x%02x", $boost, $decalc, int($maxValveSetting*255/100), int($valveOffset*255/100)); return ($hash->{IODev}{Send})->($hash->{IODev},"ConfigValve",$hash->{addr},$payload,callbackParam => "$setting,$val"); }elsif($setting eq "groupid"){ return "argument needed" if(@args == 0); return ($hash->{IODev}{Send})->($hash->{IODev},"SetGroupId",$hash->{addr}, sprintf("%02x",$args[0]) ); }elsif( $setting ~~ ["ecoTemperature", "comfortTemperature", "measurementOffset", "maximumTemperature", "minimumTemperature", "windowOpenTemperature", "windowOpenDuration" ] and $hash->{type} =~ /.*Thermostat.*/) { return "Cannot set without IODev" if(!exists($hash->{IODev})); if(!MAX_Validate($setting, $args[0])) { my $msg = "Invalid value $args[0] for $setting"; Log 1, $msg; return $msg; } my $comfortTemperature = MAX_ReadingsVal($hash,"comfortTemperature"); my $ecoTemperature = MAX_ReadingsVal($hash,"ecoTemperature"); my $maximumTemperature = MAX_ReadingsVal($hash,"maximumTemperature"); my $minimumTemperature = MAX_ReadingsVal($hash,"minimumTemperature"); my $windowOpenTemperature = MAX_ReadingsVal($hash,"windowOpenTemperature"); my $windowOpenDuration = MAX_ReadingsVal($hash,"windowOpenDuration"); my $measurementOffset = MAX_ReadingsVal($hash,"measurementOffset"); eval "\$$setting = $args[0]"; my $comfort = int(MAX_ParseTemperature($comfortTemperature)*2); my $eco = int(MAX_ParseTemperature($ecoTemperature)*2); my $max = int(MAX_ParseTemperature($maximumTemperature)*2); my $min = int(MAX_ParseTemperature($minimumTemperature)*2); my $offset = int(($measurementOffset + 3.5)*2); my $windowOpenTemp = int(MAX_ParseTemperature($windowOpenTemperature)*2); my $windowOpenTime = int($windowOpenDuration/5); my $payload = sprintf("%02x%02x%02x%02x%02x%02x%02x",$comfort,$eco,$max,$min,$offset,$windowOpenTemp,$windowOpenTime); return ($hash->{IODev}{Send})->($hash->{IODev},"ConfigTemperatures",$hash->{addr},$payload, callbackParam => "$setting,$args[0]") } elsif($setting eq "displayActualTemperature" and $hash->{type} eq "WallMountedThermostat") { return "Invalid arg" if($args[0] ne "0" and $args[0] ne "1"); return ($hash->{IODev}{Send})->($hash->{IODev},"SetDisplayActualTemperature",$hash->{addr}, sprintf("%02x",$args[0] ? 4 : 0), callbackParam => "$setting,$args[0]"); } elsif($setting eq "fake") { #Deprecated, use fakeWT and fakeSC of CUL_MAX #Resolve first argument to address return "Invalid number of arguments" if(@args == 0); my $dest = $args[0]; if(exists($defs{$dest})) { return "Destination is not a MAX device" if($defs{$dest}{TYPE} ne "MAX"); $dest = $defs{$dest}{addr}; } else { return "No MAX device with address $dest" if(!exists($modules{MAX}{defptr}{$dest})); } if($hash->{type} eq "ShutterContact") { return "Invalid number of arguments" if(@args != 2); my $state = $args[1] ? "12" : "10"; return ($hash->{IODev}{Send})->($hash->{IODev},"ShutterContactState",$dest,$state, flags => "06", src => $hash->{addr}); } elsif($hash->{type} eq "WallMountedThermostat") { return "Invalid number of arguments" if(@args != 3); return "desiredTemperature is invalid" if($args[1] < 4.5 || $args[2] > 30.5); $args[2] = 0 if($args[2] < 0); #Clamp temperature to minimum of 0 degree #Encode into binary form my $arg2 = int(10*$args[2]); #First bit is 9th bit of temperature, rest is desiredTemperature my $arg1 = (($arg2&0x100)>>1) | (int(2*$args[1])&0x7F); $arg2 &= 0xFF; #only take the lower 8 bits return ($hash->{IODev}{Send})->($hash->{IODev},"WallThermostatControl",$dest, sprintf("%02x%02x",$arg1,$arg2),flags => "04", src => $hash->{addr}); } else { return "fake does not work for device type $hash->{type}"; } } elsif($setting ~~ ["associate", "deassociate"]) { my $dest = $args[0]; my $destType; if($dest eq "fakeWallThermostat") { return "IODev is not CUL_MAX" if($hash->{IODev}->{TYPE} ne "CUL_MAX"); $dest = AttrVal($hash->{IODev}->{NAME}, "fakeWTaddr", "111111"); return "Invalid fakeWTaddr attribute set (must not be 000000)" if($dest eq "000000"); $destType = MAX_TypeToTypeId("WallMountedThermostat"); } elsif($dest eq "fakeShutterContact") { return "IODev is not CUL_MAX" if($hash->{IODev}->{TYPE} ne "CUL_MAX"); $dest = AttrVal($hash->{IODev}->{NAME}, "fakeSCaddr", "222222"); return "Invalid fakeSCaddr attribute set (must not be 000000)" if($dest eq "000000"); $destType = MAX_TypeToTypeId("ShutterContact"); } else { if(exists($defs{$dest})) { return "Destination is not a MAX device" if($defs{$dest}{TYPE} ne "MAX"); $dest = $defs{$dest}{addr}; } else { return "No MAX device with address $dest" if(!exists($modules{MAX}{defptr}{$dest})); } my $destType = MAX_TypeToTypeId($modules{MAX}{defptr}{$dest}{type}); Log 2, "Warning: Device do not have same groupid" if($hash->{groupid} != $modules{MAX}{defptr}{$dest}{groupid}); } Log GetLogLevel($hash->{NAME}, 5), "Using dest $dest, destType $destType"; if($setting eq "associate") { return ($hash->{IODev}{Send})->($hash->{IODev},"AddLinkPartner",$hash->{addr},sprintf("%s%02x", $dest, $destType)); } else { return ($hash->{IODev}{Send})->($hash->{IODev},"RemoveLinkPartner",$hash->{addr},sprintf("%s%02x", $dest, $destType)); } } elsif($setting eq "factoryReset") { if(exists($hash->{IODev}{RemoveDevice})) { #MAXLAN return ($hash->{IODev}{RemoveDevice})->($hash->{IODev},$hash->{addr}); } else { #CUL_MAX return ($hash->{IODev}{Send})->($hash->{IODev},"Reset",$hash->{addr}); } } elsif($setting eq "wakeUp") { return ($hash->{IODev}{Send})->($hash->{IODev},"WakeUp",$hash->{addr}, 0x3F); } elsif($setting eq "weekProfile" and $hash->{type} =~ /.*Thermostat.*/) { return "Number of arguments must be even" if(@args%2 == 1); for(my $i = 0; $i < @args; $i += 2) { return "Expected day, got $args[$i]" if(!exists($decalcDaysInv{$args[$i]})); my $day = $decalcDaysInv{$args[$i]}; my @controlpoints = split(',',$args[$i+1]); return "Not more than 13 control points are allowed!" if(@controlpoints > 13*2); my $newWeekprofilePart = ""; for(my $j = 0; $j < 13*2; $j += 2) { if( $j >= @controlpoints ) { $newWeekprofilePart .= "4520"; next; } my ($hour, $min); if($j + 1 == @controlpoints) { $hour = 0; $min = 0; } else { ($hour, $min) = ($controlpoints[$j+1] =~ /^(\d{1,2}):(\d{1,2})$/); } my $temperature = $controlpoints[$j]; return "Invalid time: $controlpoints[$j+1]" if(!defined($hour) || !defined($min) || $hour > 23 || $min > 59); return "Invalid temperature" if(!validTemperature($temperature)); $temperature = MAX_ParseTemperature($temperature); #replace "on" and "off" by their values $newWeekprofilePart .= sprintf("%04x", (int($temperature*2) << 9) | int(($hour * 60 + $min)/5)); } Log GetLogLevel($hash->{NAME}, 5), "New Temperature part for $day: $newWeekprofilePart"; #Each day has 2 bytes * 13 controlpoints = 26 bytes = 52 hex characters #we don't have to update the rest, because the active part is terminated by the time 0:00 #First 7 controlpoints (2*7=14 bytes => 2*2*7=28 hex characters ) ($hash->{IODev}{Send})->($hash->{IODev},"ConfigWeekProfile",$hash->{addr}, sprintf("0%1d%s", $day, substr($newWeekprofilePart,0,2*2*7)), callbackParam => "$day,0,".substr($newWeekprofilePart,0,2*2*7)); #And then the remaining 6 ($hash->{IODev}{Send})->($hash->{IODev},"ConfigWeekProfile",$hash->{addr}, sprintf("1%1d%s", $day, substr($newWeekprofilePart,2*2*7,2*2*6)), callbackParam => "$day,1,".substr($newWeekprofilePart,2*2*7,2*2*6)) if(@controlpoints > 2*7); } Log GetLogLevel($hash->{NAME}, 5), "New weekProfile: " . MAX_ReadingsVal($hash, ".weekProfile"); }else{ my $templist = "off,".join(",",map { sprintf("%2.1f",$_/2) } (10..60)) . ",on"; my $ret = "Unknown argument $setting, choose one of wakeUp factoryReset groupid"; my $assoclist; #Build list of devices which this device can be associated to if($hash->{type} =~ /HeatingThermostat.*/) { $assoclist = join(",", map { defined($_->{type}) && $_->{type} ~~ ["HeatingThermostat", "HeatingThermostatPlus", "WallMountedThermostat", "ShutterContact"] && $_ != $hash ? $_->{NAME} : () } values %{$modules{MAX}{defptr}}); if($hash->{IODev}->{TYPE} eq "CUL_MAX") { $assoclist .= "," if(length($assoclist)); $assoclist .= "fakeWallThermostat,fakeShutterContact"; } } elsif($hash->{type} ~~ ["ShutterContact", "WallMountedThermostat"]) { $assoclist = join(",", map { defined($_->{type}) && $_->{type} =~ /HeatingThermostat.*/ ? $_->{NAME} : () } values %{$modules{MAX}{defptr}}); } if($hash->{type} =~ /HeatingThermostat.*/) { #Create numbers from 4.5 to 30.5 my $templistOffset = join(",",map { sprintf("%2.1f",($_-7)/2) } (0..14)); my $boostDurVal = join(",", values(%boost_durations)); return "$ret associate:$assoclist deassociate:$assoclist desiredTemperature:eco,comfort,boost,auto,$templist ecoTemperature:$templist comfortTemperature:$templist measurementOffset:$templistOffset maximumTemperature:$templist minimumTemperature:$templist windowOpenTemperature:$templist windowOpenDuration boostDuration:$boostDurVal boostValveposition decalcification maxValveSetting valveOffset"; } elsif($hash->{type} eq "WallMountedThermostat") { return "$ret associate:$assoclist deassociate:$assoclist displayActualTemperature:0,1 desiredTemperature:eco,comfort,boost,auto,$templist ecoTemperature:$templist comfortTemperature:$templist maximumTemperature:$templist"; } elsif($hash->{type} eq "ShutterContact") { return "$ret associate:$assoclist deassociate:$assoclist"; } else { return $ret; } } } ############################# sub MAX_ParseDateTime($$$) { my ($byte1,$byte2,$byte3) = @_; my $day = $byte1 & 0x1F; my $month = (($byte1 & 0xE0) >> 4) | ($byte2 >> 7); my $year = $byte2 & 0x3F; my $time = ($byte3 & 0x3F); if($time%2){ $time = int($time/2).":30"; }else{ $time = int($time/2).":00"; } return { "day" => $day, "month" => $month, "year" => $year, "time" => $time, "str" => "$day.$month.$year $time" }; } ############################# sub MAX_Parse($$) { my ($hash, $msg) = @_; my ($MAX,$isToMe,$msgtype,$addr,@args) = split(",",$msg); #$isToMe is 1 if the message was direct at the device $hash, and 0 #if we just snooped a message directed at a different device (by CUL_MAX). return () if($MAX ne "MAX"); Log 5, "MAX_Parse $msg"; #Find the device with the given addr my $shash = $modules{MAX}{defptr}{$addr}; if(!$shash) { my $devicetype = undef; $devicetype = $args[0] if($msgtype eq "define"); $devicetype = "ShutterContact" if($msgtype eq "ShutterContactState"); $devicetype = "Cube" if($msgtype eq "CubeClockState" or $msgtype eq "CubeConnectionState"); $devicetype = "WallMountedThermostat" if($msgtype ~~ ["WallThermostatConfig","WallThermostatState","WallThermostatControl"]); $devicetype = "HeatingThermostat" if($msgtype ~~ ["HeatingThermostatConfig", "ThermostatState"]); if($devicetype) { return "UNDEFINED MAX_$addr MAX $devicetype $addr"; } else { Log 2, "Got message for undefined device, and failed to guess type from msg '$msgtype' - ignoring"; return $hash->{NAME}; } } #if $isToMe is true, then the message was directed at device $hash, thus we can also use it for sending if($isToMe) { $shash->{IODev} = $hash; $shash->{backend} = $hash->{NAME}; #for user information } readingsBeginUpdate($shash); if($msgtype eq "define"){ my $devicetype = $args[0]; Log 1, "Device changed type from $shash->{type} to $devicetype" if($shash->{type} ne $devicetype); if(@args > 1){ my $serial = $args[1]; Log 1, "Device changed serial from $shash->{serial} to $serial" if($shash->{serial} and ($shash->{serial} ne $serial)); $shash->{serial} = $serial; } $shash->{groupid} = $args[2]; $shash->{IODev} = $hash; } elsif($msgtype eq "ThermostatState") { my ($bits2,$valveposition,$desiredTemperature,$until1,$until2,$until3) = unpack("aCCCCC",pack("H*",$args[0])); my $mode = vec($bits2, 0, 2); # my $dstsetting = vec($bits2, 3, 1); #is automatically switching to DST activated my $langateway = vec($bits2, 4, 1); #?? my $panel = vec($bits2, 5, 1); #1 if the heating thermostat is locked for manually setting the temperature at the device my $rferror = vec($bits2, 6, 1); #communication with link partner (what does that mean?) my $batterylow = vec($bits2, 7, 1); #1 if battery is low my $untilStr = defined($until3) ? MAX_ParseDateTime($until1,$until2,$until3)->{str} : ""; my $measuredTemperature = defined($until2) ? ((($until1 &0x01)<<8) + $until2)/10 : 0; #If the control mode is not "temporary", the cube sends the current (measured) temperature $measuredTemperature = "" if($mode == 2 || $measuredTemperature == 0); $untilStr = "" if($mode != 2); $desiredTemperature = ($desiredTemperature&0x7F)/2.0; #convert to degree celcius Log GetLogLevel($shash->{NAME}, 5), "battery $batterylow, rferror $rferror, panel $panel, langateway $langateway, dstsetting $dstsetting, mode $mode, valveposition $valveposition %, desiredTemperature $desiredTemperature, until $untilStr, curTemp $measuredTemperature"; #Very seldomly, the HeatingThermostat sends us temperatures like 0.2 or 0.3 degree Celcius - ignore them $measuredTemperature = "" if($measuredTemperature ne "" and $measuredTemperature < 1); $shash->{mode} = $mode; $shash->{rferror} = $rferror; $shash->{dstsetting} = $dstsetting; if($mode eq "temporary"){ $shash->{until} = "$untilStr"; }else{ delete($shash->{until}); } readingsBulkUpdate($shash, "mode", $ctrl_modes[$mode] ); readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok"); #The formatting of desiredTemperature must match with in MAX_Set:$templist #Sometime we get an MAX_Parse MAX,1,ThermostatState,01090d,180000000000, where desiredTemperature is 0 - ignore it readingsBulkUpdate($shash, "desiredTemperature", sprintf("%2.1f",$desiredTemperature)) if($desiredTemperature != 0); readingsBulkUpdate($shash, "valveposition", $valveposition); if($measuredTemperature ne "") { readingsBulkUpdate($shash, "temperature", sprintf("%2.1f",$measuredTemperature)); } }elsif($msgtype ~~ ["WallThermostatState", "WallThermostatControl" ]){ my ($bits2,$displayActualTemperature,$desiredTemperature,$null1,$heaterTemperature,$null2,$temperature); if( length($args[0]) == 4 ) { #WallThermostatControl #This is the message that WallMountedThermostats send to paired HeatingThermostats ($desiredTemperature,$temperature) = unpack("CC",pack("H*",$args[0])); } elsif( length($args[0]) == 6 or length($args[0]) == 14 or length($args[0]) == 12) { #WallThermostatState #len=14: This is the message we get from the Cube over MAXLAN and which is probably send by WallMountedThermostats to the Cube #len=12: Payload of an Ack message, last field "temperature" is missing #len=6 : Payload of an Ack message, last four fields (especially $heaterTemperature and $temperature) are missing ($bits2,$displayActualTemperature,$desiredTemperature,$null1,$heaterTemperature,$null2,$temperature) = unpack("aCCCCCC",pack("H*",$args[0])); #$heaterTemperature/10 is the temperature measured by a paired HeatingThermostat #we don't do anything with it here, because this value also appears as temperature in the HeatingThermostat's ThermostatState message my $mode = vec($bits2, 0, 2); # my $dstsetting = vec($bits2, 3, 1); #is automatically switching to DST activated my $langateway = vec($bits2, 4, 1); #?? my $panel = vec($bits2, 5, 1); #1 if the heating thermostat is locked for manually setting the temperature at the device my $rferror = vec($bits2, 6, 1); #communication with link partner (what does that mean?) my $batterylow = vec($bits2, 7, 1); #1 if battery is low Log 2, "Warning: WallThermostatState null1: $null1 null2: $null2 should be both zero" if($null1 != 0 || $null2 != 0); Log GetLogLevel($shash->{NAME}, 5), "battery $batterylow, rferror $rferror, panel $panel, langateway $langateway, dstsetting $dstsetting, mode $mode, displayActualTemperature $displayActualTemperature, heaterTemperature $heaterTemperature"; $shash->{rferror} = $rferror; readingsBulkUpdate($shash, "mode", $ctrl_modes[$mode] ); readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok"); readingsBulkUpdate($shash, "displayActualTemperature", ($displayActualTemperature) ? 1 : 0); } else { Log 2, "Invalid $msgtype packet" } $desiredTemperature = ($desiredTemperature &0x7F)/2.0; #convert to degree celcius if(defined($temperature)) { $temperature = ((($desiredTemperature &0x80)<<1) + $temperature)/10; # auch Temperaturen über 25.5 °C werden angezeigt ! Log GetLogLevel($shash->{NAME}, 5), "desiredTemperature $desiredTemperature, temperature $temperature"; readingsBulkUpdate($shash, "temperature", sprintf("%2.1f",$temperature)); } else { Log GetLogLevel($shash->{NAME}, 5), "desiredTemperature $desiredTemperature" } #This formatting must match with in MAX_Set:$templist readingsBulkUpdate($shash, "desiredTemperature", sprintf("%2.1f",$desiredTemperature)); }elsif($msgtype eq "ShutterContactState"){ my $bits = pack("H2",$args[0]); my $isopen = vec($bits,0,2) == 0 ? 0 : 1; my $unkbits = vec($bits,2,4); my $rferror = vec($bits,6,1); my $batterylow = vec($bits,7,1); Log GetLogLevel($shash->{NAME}, 5), "ShutterContact isopen $isopen, rferror $rferror, battery $batterylow, unkbits $unkbits"; $shash->{rferror} = $rferror; readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok"); readingsBulkUpdate($shash,"onoff",$isopen); }elsif($msgtype eq "PushButtonState") { my ($bits2, $onoff) = unpack("CC",pack("H*",$args[0])); #The meaning of $bits2 is completly guessed based on similarity to other devices, TODO: confirm my $rferror = vec($bits2, 6, 1); #communication with link partner (what does that mean?) my $batterylow = vec($bits2, 7, 1); #1 if battery is low readingsBulkUpdate($shash, "battery", $batterylow ? "low" : "ok"); readingsBulkUpdate($shash,"onoff",$onoff); }elsif($msgtype eq "CubeClockState"){ my $clockset = $args[0]; $shash->{clocknotset} = !$clockset; }elsif($msgtype eq "CubeConnectionState"){ my $connected = $args[0]; readingsBulkUpdate($shash, "connection", $connected); } elsif($msgtype ~~ ["HeatingThermostatConfig", "WallThermostatConfig"]) { readingsBulkUpdate($shash, "ecoTemperature", sprintf("%2.1f",$args[0])); readingsBulkUpdate($shash, "comfortTemperature", sprintf("%2.1f",$args[1])); readingsBulkUpdate($shash, "maximumTemperature", sprintf("%2.1f",$args[2])); readingsBulkUpdate($shash, "minimumTemperature", sprintf("%2.1f",$args[3])); if($shash->{type} =~ /HeatingThermostat.*/) { readingsBulkUpdate($shash, "boostValveposition", $args[4]); readingsBulkUpdate($shash, "boostDuration", $boost_durations{$args[5]}); readingsBulkUpdate($shash, "measurementOffset", sprintf("%2.1f",$args[6])); readingsBulkUpdate($shash, "windowOpenTemperature", sprintf("%2.1f",$args[7])); readingsBulkUpdate($shash, "windowOpenDuration", $args[8]); readingsBulkUpdate($shash, "maxValveSetting", $args[9]); readingsBulkUpdate($shash, "valveOffset", $args[10]); readingsBulkUpdate($shash, "decalcification", "$decalcDays{$args[11]} $args[12]:00"); readingsBulkUpdate($shash, ".weekProfile", $args[13]); } else { readingsBulkUpdate($shash, ".weekProfile", $args[4]); } MAX_ParseWeekProfile($shash); } elsif($msgtype eq "Error") { if(@args == 0) { delete $shash->{ERROR} if(exists($shash->{ERROR})); } else { $shash->{ERROR} = join(",",$args[0]); } } elsif($msgtype eq "AckConfigWeekProfile") { my ($day, $part, $profile) = @args; my $curWeekProfile = MAX_ReadingsVal($hash, ".weekProfile"); substr($curWeekProfile, $day*52+$part*2*2*7, length($profile)) = $profile; readingsBulkUpdate($shash, ".weekProfile", $curWeekProfile); MAX_ParseWeekProfile($shash); } elsif($msgtype ~~ ["AckConfigValve", "AckConfigTemperatures", "AckSetDisplayActualTemperature" ]) { readingsBulkUpdate($shash, $args[0], $args[1]); } elsif($msgtype eq "Ack") { #The payload of an Ack is a 2-digit hex number (being "01" for okey and "81" for "invalid command/argument" if($isToMe and (unpack("C",pack("H*",$args[0])) & 0x80)) { my $device = $addr; $device = $modules{MAX}{defptr}{$device}{NAME} if(exists($modules{MAX}{defptr}{$device})); Log 1, "Device $device answered with: Invalid command/argument"; } #with unknown meaning plus the data of a State broadcast from the same device #For HeatingThermostats, it does not contain the last three "until" bytes (or measured temperature) if($shash->{type} =~ /HeatingThermostat.*/ ) { return MAX_Parse($hash, "MAX,$isToMe,ThermostatState,$addr,". substr($args[0],2)); } elsif($shash->{type} eq "WallMountedThermostat") { return MAX_Parse($hash, "MAX,$isToMe,WallThermostatState,$addr,". substr($args[0],2)); } elsif($shash->{type} eq "ShutterContact") { return MAX_Parse($hash, "MAX,$isToMe,ShutterContactState,$addr,". substr($args[0],2)); } elsif($shash->{type} eq "PushButton") { return MAX_Parse($hash, "MAX,$isToMe,PushButtonState,$addr,". substr($args[0],2)); } elsif($shash->{type} eq "Cube") { ; #Payload is always "00" } else { Log 2, "MAX_Parse: Don't know how to interpret Ack payload for $shash->{type}"; } } else { Log 1, "MAX_Parse: Unknown message $msgtype"; } #Build state READING my $state = "waiting for data"; if(exists($shash->{READINGS})) { $state = $shash->{READINGS}{connection}{VAL} ? "connected" : "not connected" if(exists($shash->{READINGS}{connection})); $state = "$shash->{READINGS}{desiredTemperature}{VAL} °C" if(exists($shash->{READINGS}{desiredTemperature})); $state = $shash->{READINGS}{onoff}{VAL} ? "opened" : "closed" if(exists($shash->{READINGS}{onoff})); } $state .= " (clock not set)" if($shash->{clocknotset}); $state .= " (auto)" if(exists($shash->{mode}) and $shash->{mode} eq "auto"); #Don't print this: it's the standard mode #$state .= " (manual)" if(exists($shash->{mode}) and $shash->{mode} eq "manual"); $state .= " (until ".$shash->{until}.")" if(exists($shash->{mode}) and $shash->{mode} eq "temporary" ); $state .= " (battery low)" if($shash->{batterylow}); $state .= " (rf error)" if($shash->{rferror}); readingsBulkUpdate($shash, "state", $state); readingsEndUpdate($shash, 1); return $shash->{NAME} } 1; =pod =begin html

MAX

=end html =cut