################################################################ # # Copyright notice # # (c) 2008 Dr. Boris Neubert (omega@online.de) # # This script is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # This copyright notice MUST APPEAR in all copies of the script! # ################################################################ # $Id$ # # Internals introduced in this module: # MODEL distinguish between different X10 device types # BRIGHT brightness level of dimmer devices in units of microdims (0..210) # # Readings introduced in this module: # state function and argument of last command # onoff inherited from switch interface (0= on, 1= off) # dimmer inherited from dimmer interface (0= dark, 100= bright) # # Setters introduced in this module: # on inherited from switch interface # off inherited from switch interface # dimmer inherited from dimmer interface (0= dark, 100= bright) # dimdown inherited from dimmer interface # dimup inherited from dimmer interface # package main; use strict; use warnings; my %functions = ( ALL_UNITS_OFF => "all_units_off", ALL_LIGHTS_ON => "all_lights_on", ON => "on", OFF => "off", DIM => "dimdown", BRIGHT => "dimup", ALL_LIGHTS_OFF => "all_lights_off", EXTENDED_CODE => "", HAIL_REQUEST => "", HAIL_ACK => "", PRESET_DIM1 => "", PRESET_DIM2 => "", EXTENDED_DATA_TRANSFER => "", STATUS_ON => "", STATUS_OFF => "", STATUS_REQUEST => "", ); my %snoitcnuf; # the reverse of the above my %functions_rewrite = ( "all_units_off" => "off", "all_lights_on" => "on", "all_lights_off" => "off", ); my %functions_snd = qw( ON 0010 OFF 0011 DIM 0100 BRIGHT 0101 ); my %housecodes_snd = qw(A 0110 B 1110 C 0010 D 1010 E 0001 F 1001 G 0101 H 1101 I 0111 J 1111 K 0011 L 1011 M 0000 N 1000 O 0100 P 1100); my %unitcodes_snd = qw( 1 0110 2 1110 3 0010 4 1010 5 0001 6 1001 7 0101 8 1101 9 0111 10 1111 11 0011 12 1011 13 0000 14 1000 15 0100 16 1100); my %functions_set = ( "on" => 0, "off" => 0, "dimup" => 1, "dimdown" => 1, "dimto" => 1, "on-till" => 1, "on-for-timer" => 1, "all_units_off" => 0, "all_units_on" => 0, "all_lights_off" => 0, "all_lights_on" => 0, ); my %models = ( lm12 => 'dimmer', lm15 => 'switch', am12 => 'switch', tm13 => 'switch', ); my %interfaces = ( lm12 => 'dimmer', lm15 => 'switch_passive', am12 => 'switch_passive', tm13 => 'switch_passive', ); my @lampmodules = ('lm12','lm15'); # lamp modules sub X10_Initialize($) { my ($hash) = @_; foreach my $k (keys %functions) { $snoitcnuf{$functions{$k}}= $k; } $hash->{Match} = "^X10:[A-P];"; $hash->{SetFn} = "X10_Set"; $hash->{StateFn} = "X10_SetState"; $hash->{DefFn} = "X10_Define"; $hash->{UndefFn} = "X10_Undef"; $hash->{ParseFn} = "X10_Parse"; $hash->{AttrList} = "IODev do_not_notify:1,0 " . "dummy:1,0 showtime:1,0 model:lm12,lm15,am12,tm13 " . $readingFnAttributes; } ##################################### sub X10_SetState($$$$) { my ($hash, $tim, $vt, $val) = @_; return undef; } ############################# sub X10_StateMachine($$$$) { my($hash, $time, $function, $argument)= @_; # the following changes between (onoff,bright) states were # experimentally observed for a Busch Timac Ferndimmer 2265 # bright and argument are measured in brightness steps # from 0 (0%) to 210 (100%). # for convenience, we connect the off state with a 210 bright state # # initial on off dimup d dimdown d # ------------------------------------------------------------------------- # (on,x) -> (on,x) (off,210) (on,x+d) (on,x-d) # (off,210) -> (on,210) (off,210) (on,210) (on,210-d) my $onoff; my $bright; if(defined($hash->{ONOFF})) { $onoff= $hash->{ONOFF}; } else { $onoff= 0; } if(defined($hash->{BRIGHT})) { $bright= $hash->{BRIGHT}; } else { $bright= 0; } #Log3 $hash, 1, $hash->{NAME} . " initial state ($onoff,$bright)"; if($onoff) { # initial state (on,bright) if($function eq "on") { } elsif($function eq "off") { $onoff= 0; $bright= 210; } elsif($function eq "dimup") { $bright+= $argument; if($bright> 210) { $bright= 210 }; } elsif($function eq "dimdown") { $bright-= $argument; if($bright< 0) { $bright= 0 }; } } else { # initial state (off,bright) if($function eq "on") { $onoff= 1; $bright= 210; } elsif($function eq "off") { $onoff= 0; $bright= 210; } elsif($function eq "dimup") { $onoff= 1; $bright= 210; } elsif($function eq "dimdown") { $onoff= 1; $bright= 210-$argument; if($bright< 0) { $bright= 0 }; } } #Log3 $hash, 1, $hash->{NAME} . " final state ($onoff,$bright)"; $hash->{ONOFF}= $onoff; $hash->{BRIGHT}= $bright; #$hash->{READINGS}{onoff}{TIME}= $time; #$hash->{READINGS}{onoff}{VAL}= $onoff; readingsBeginUpdate($hash); readingsBulkUpdate($hash,'onoff',$onoff,1,$time); #$hash->{READINGS}{dimmer}{TIME}= $time; #$hash->{READINGS}{dimmer}{VAL}= int(1000.0*$bright/210.0+0.5)/10.0; readingsBulkUpdate($hash,'dimmer',int(1000.0*$bright/210.0+0.5)/10.0,1,$time); readingsEndUpdate($hash,1); return; } ############################# sub X10_LevelToDims($) { # 22= 100% my ($level)= @_; my $dim= int(22*$level/100.0+0.5); return $dim; } ############################# sub X10_Do_On_Till($@) { my ($hash, @a) = @_; return "Timespec (HH:MM[:SS]) needed for the on-till command" if(@a != 3); my ($err, $hr, $min, $sec, $fn) = GetTimeSpec($a[2]); return $err if($err); my @lt = localtime; my $hms_till = sprintf("%02d:%02d:%02d", $hr, $min, $sec); my $hms_now = sprintf("%02d:%02d:%02d", $lt[2], $lt[1], $lt[0]); if($hms_now ge $hms_till) { Log3 $hash, 4, "on-till: won't switch as now ($hms_now) is later than $hms_till"; return ""; } if($modules{X10}{ldata}{$a[0]}) { CommandDelete(undef, $a[0] . "_timer"); delete $modules{FS20}{ldata}{$a[0]}; } $modules{X10}{ldata}{$a[0]} = "$hms_till"; my @b = ($a[0], "on"); X10_Set($hash, @b); CommandDefine(undef, $hash->{NAME} . "_timer at $hms_till set $a[0] off"); } ############################# sub X10_Do_On_For_Timer($@) { my ($hash, @a) = @_; return "Timespec (HH:MM[:SS]) needed for the on-for-timer command" if(@a != 3); my ($err, $hr, $min, $sec, $fn) = GetTimeSpec($a[2]); return $err if($err); my $hms_for_timer = sprintf("+%02d:%02d:%02d", $hr, $min, $sec); if($modules{X10}{ldata}{$a[0]}) { CommandDelete(undef, $a[0] . "_timer"); delete $modules{FS20}{ldata}{$a[0]}; } $modules{X10}{ldata}{$a[0]} = "$hms_for_timer"; my @b = ($a[0], "on"); X10_Set($hash, @b); CommandDefine(undef, $hash->{NAME} . "_timer at $hms_for_timer set $a[0] off"); } ################################### sub X11_Write($$$) { my ($hash, $function, $dim)= @_; my $name = $hash->{NAME}; my $housecode= $hash->{HOUSE}; my $unitcode = $hash->{UNIT}; my $x10func = $snoitcnuf{$function}; undef $function; # do not use after this point my $prefix= "X10 device $name:"; Log3 $name, 5, "$prefix sending X10:$housecode;$unitcode;$x10func $dim"; my ($hc_b, $hu_b, $hf_b); my ($hc, $hu, $hf); # Header:Code, Address $hc_b = "00000100"; # 0x04 $hc = pack("B8", $hc_b); $hu_b = $housecodes_snd{$housecode} . $unitcodes_snd{$unitcode}; $hu = pack("B8", $hu_b); IOWrite($hash, $hc, $hu); # Header:Code, Function $hc_b = substr(unpack('B8', pack('C', $dim)), 3) . # dim, 0..22 "110"; # always 110 $hc = pack("B8", $hc_b); $hf_b = $housecodes_snd{$housecode} . $functions_snd{$x10func}; $hf = pack("B8", $hf_b); IOWrite($hash, $hc, $hf); } ################################### sub X10_Set($@) { my ($hash, @a) = @_; my $ret = undef; my $na = int(@a); # initialization and sanity checks return "no set value specified" if($na < 2); my $name= $hash->{NAME}; my $function= $a[1]; my $nrparams= $functions_set{$function}; return "Unknown argument $function, choose one of " . join(" ", sort keys %functions_set) if(!defined($nrparams)); return "Wrong number of parameters" if($na != 2+$nrparams); # special for on-till return X10_Do_On_Till($hash, @a) if($function eq "on-till"); # special for on-for-timer return X10_Do_On_For_Timer($hash, @a) if($function eq "on-for-timer"); # argument evaluation my $model= $hash->{MODEL}; my $dim= 0; if($function =~ m/^dim/) { return "Cannot dim $name (model $model)" if($models{$model} ne "dimmer"); my $arg= $a[2]; return "Wrong argument $arg, use 0..100" if($arg !~ m/^[0-9]{1,3}$/); return "Wrong argument $arg, use 0..100" if($arg>100); if($function eq "dimto") { # translate dimmer command to dimup/dimdown command my $bright= 210; if(defined($hash->{BRIGHT})) { $bright= $hash->{BRIGHT} }; $arg= $arg-100.0*$bright/210.0; if($arg> 0) { $function= "dimup"; $dim= X10_LevelToDims($arg); } else { $function= "dimdown"; $dim= X10_LevelToDims(-$arg); } } else { $dim= X10_LevelToDims($arg); } # the meaning of $dim= 0, 1 is unclear # if we encounter the need for dimming by such a small amount, we # ignore it if($dim< 2) { return "Dim amount too small" }; }; # send command to CM11 X11_Write($hash, $function, $dim) if(!IsDummy($a[0])); my $v = join(" ", @a); Log3 $a[0], 2, "X10 set $v"; (undef, $v) = split(" ", $v, 2); # Not interested in the name... my $tn = TimeNow(); $hash->{CHANGED}[0] = $v; #$hash->{STATE} = $v; #$hash->{READINGS}{state}{TIME} = $tn; #$hash->{READINGS}{state}{VAL} = $v; readingsSingleUpdate($hash,'state',$v,1,$tn); X10_StateMachine($hash, $tn, $function, int(210.0*$dim/22.0+0.5)); return undef; } ############################# sub X10_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "wrong syntax: define X10 model housecode unitcode" if(int(@a)!= 5); my $model= $a[2]; return "Define $a[0]: wrong model: specify one of " . join ",", sort keys %models if(!grep { $_ eq $model} keys %models); my $housecode = $a[3]; return "Define $a[0]: wrong housecode format: specify a value ". "from A to P" if($housecode !~ m/^[A-P]$/i); my $unitcode = $a[4]; return "Define $a[0]: wrong unitcode format: specify a value " . "from 1 to 16" if( ($unitcode<1) || ($unitcode>16) ); $hash->{MODEL} = $model; $hash->{HOUSE} = $housecode; $hash->{UNIT} = $unitcode; $hash->{internals}{interfaces}= $interfaces{$model}; if(defined($modules{X10}{defptr}{$housecode}{$unitcode})) { return "Error: duplicate X10 device $housecode $unitcode definition " . $hash->{NAME} . " (previous: " . $modules{X10}{defptr}{$housecode}{$unitcode}->{NAME} .")"; } $modules{X10}{defptr}{$housecode}{$unitcode}= $hash; AssignIoPort($hash); } ############################# sub X10_Undef($$) { my ($hash, $name) = @_; if( defined($hash->{HOUSE}) && defined($hash->{UNIT}) ) { delete($modules{X10}{defptr}{$hash->{HOUSE}}{$hash->{UNIT}}); } return undef; } ############################# sub X10_Parse($$) { my ($hash, $msg) = @_; # message example: X10:N;1 12;OFF (undef, $msg)= split /:/, $msg, 2; # strip off "X10" my ($housecode,$unitcodes,$command)= split /;/, $msg, 4; my @list; # list of selected devices # # command evaluation # my ($x10func,$arg)= split / /, $command, 2; my $function= $functions{$x10func}; # translate, eg BRIGHT -> dimup undef $x10func; # do not use after this point # the following code sequence converts an all on/off command into # a sequence of simple on/off commands for all defined devices my $all_lights= ($function=~ m/^all_lights_/); my $all_units= ($function=~ m/^all_units_/); if($all_lights || $all_units) { $function= $functions_rewrite{$function}; # translate, all_lights_on -> on $unitcodes= ""; foreach my $unitcode (keys %{ $modules{X10}{defptr}{$housecode} } ) { my $h= $modules{X10}{defptr}{$housecode}{$unitcode}; my $islampmodule= grep { $_ eq $h->{MODEL} } @lampmodules; if($all_units || $islampmodule ) { $unitcodes.= " " if($unitcodes ne ""); $unitcodes.= $h->{UNIT}; } } # no units for that housecode if($unitcodes eq "") { Log3 $hash, 3, "X10 No units with housecode $housecode, command $command, " . "please define one"; push(@list, "UNDEFINED X10_$housecode X10 lm15 $housecode ?"); return @list; } } # apply to each unit in turn my @unitcodes= split / /, $unitcodes; if(!int(@unitcodes)) { # command without unitcodes, this happens when a single on/off is sent # but no unit was previously selected Log3 $hash, 3, "X10 No unit selected for housecode $housecode, command $command"; push(@list, "UNDEFINED X10_$housecode X10 lm15 $housecode ?"); return @list; } # function rewriting my $value= $function; return @list if($value eq ""); # function not evaluated # function determined, add argument if( defined($arg) ) { # received dims from 0..210 my $dim= $arg; $value = "$value $dim" ; } my $unknown_unitcodes= ''; my $tn= TimeNow(); foreach my $unitcode (@unitcodes) { my $h= $modules{X10}{defptr}{$housecode}{$unitcode}; if($h) { my $name= $h->{NAME}; $h->{CHANGED}[0] = $value; #$h->{STATE} = $value; #$h->{READINGS}{state}{TIME} = $tn; #$h->{READINGS}{state}{VAL} = $value; readingsSingleUpdate($h,'state',$value,1,$tn); X10_StateMachine($h, $tn, $function, $arg); Log3 $hash, 2, "X10 $name $value"; push(@list, $name); } else { Log3 $hash, 3, "X10 Unknown device $housecode $unitcode, command $command, " . "please define it"; push(@list, "UNDEFINED X10_$housecode X10 lm15 $housecode $unitcode"); } } return @list; } 1; __END__ =pod =item summary devices communicating via the X10 protocol =item summary_DE Anbindung von X10-Geräten =begin html

X10

=end html =cut