################################################################
#
#  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: 20_X10.pm 28298 2023-12-20 08:52:44Z neubert $
# 2023-12-19 mgmino

#
# 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,
                      "all_units_off" => 0,
                      "all_units_on" => 0,
                      "all_lights_off" => 0,
                      "all_lights_on" => 0,
                      'off-till' => 10,
                      'on-till' => 11,
                      'off-till-overnight' => 20,
                      'on-till-overnight' => 21,
                      'off-for-timer' => 30,
                      'on-for-timer' => 31,
                    );

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
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 $na = int(@a);
  my $name= $hash->{NAME};
  my $function= $a[1];

  # initialization and sanity checks
  return "$name $function -> no set value specified" if($na < 2);

  my $nrparams= $functions_set{$function};
  return "$name $function -> Unknown argument, choose one of " .
          join(' ', sort keys %functions_set) if(!defined($nrparams));

  if ($nrparams > 9) { # -till or -for-timer
	return "$name $function -> Timespec (HH:MM[:SS]) missing"  if($na != 3);

	my ($err, $hr, $min, $sec, $fn) = GetTimeSpec($a[2]);
	my $hms_spec;
	if($err) { # not a time spec
	  if($a[2] =~ m/^\d+$/ and $nrparams > 21) { #if for-timer and natural number interpret as seconds
		$hms_spec = sprintf("%02d:%02d:%02d", $a[2]/3600, $a[2]/60%60, $a[2]%60);
	  } else {
		return "$name $function $a[2] -> Not timespec HH:MM[:SS] or {perlcode}";
	  }
	} else {
	  $hms_spec = sprintf("%02d:%02d:%02d", $hr, $min, $sec);
	}

	my @lt = localtime;
	my $hms_now = sprintf("%02d:%02d:%02d", $lt[2], $lt[1], $lt[0]);
	if($nrparams < 20 and $hms_now ge $hms_spec) { #only test for -till
	  Log3 $hash, 4, "$name $function $hms_spec aborted -> before current time ($hms_now)";
	  return undef;
	}

	if($modules{X10}{ldata}{$a[0]}) {
		CommandDelete(undef, $a[0] . '_timer');
		delete $modules{X10}{ldata}{$a[0]};
	}
	$modules{X10}{ldata}{$a[0]} = $hms_spec;

	my $cond = $nrparams % 2 ? 'on' : 'off';
	my @b = ($a[0], $cond);
	X10_Set($hash, @b);
	my $notCond = $nrparams % 2 ? 'off' : 'on';
	my $forTimer = $nrparams > 21 ? '+' : ''; #only for-timer
	CommandDefine(undef, "${name}_timer at $forTimer$hms_spec set $a[0] $notCond");
	return undef;
  }

  return "$name $function -> Incorrect number of parameters"  if($na != 2+$nrparams);

  # 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 "Incorrect argument $arg, use 0..100" if($arg !~ m/^[0-9]{1,3}$/);
    return "Incorrect 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 <name> 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&auml;ten
=begin html

<a id="X10"></a>
<h3>X10</h3>
<ul>
  <a id="X10-define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; X10 &lt;model&gt; &lt;housecode&gt;
          &lt;unitcode&gt;</code>
    <br><br>

   Defines an X10 device via its model, housecode and unitcode.<br><br>

   Notes:
   <ul>
   <li><code>&lt;model&gt;</code> is one of
      <ul>
        <li><code>lm12</code>: lamp module, dimmable</li>
        <li><code>lm15</code>: lamp module, not dimmable</li>
        <li><code>am12</code>: appliance module, not dimmable</li>
        <li><code>tm12</code>: tranceiver module, not dimmable. Its
            unitcode is 1.</li>
      </ul>
      Model determines whether a dim command is reasonable to be sent
      or not.</li>
   <li><code>&lt;housecode&gt;</code> ranges from A to P.</li>
   <li><code>&lt;unitcode&gt;</code> ranges from 1 to 16.</li>
   </ul>
   <br>

    Examples:
    <ul>
      <code>define lamp1 X10 lm12 N 10</code><br>
      <code>define pump X10 am12 B 7</code><br>
      <code>define lamp2 X10 lm15 N 11</code><br>
    </ul>
  </ul>
  <br>

  <a id="X10-set"></a>
  <b>Set </b>
  <ul>
    <code>set &lt;name&gt; &lt;value&gt; [&lt;argument&gt]</code>
    <br><br>
    where <code>value</code> is one of:<br>
    <pre>
    dimdown            # requires argument, see the note
    dimup              # requires argument, see the note
    off
    on
    on-till            # Special, see the note
    on-till-overnight  # Special, see the note
    off-till           # Special, see the note
    off-till-overnight # Special, see the note
    on-for-timer       # Special, see the note
    off-for-timer      # Special, see the note
    </pre>
    Examples:
    <ul>
      <code>set lamp1 dimup 10</code><br>
      <code>set lamp1,lamp2 off</code><br>
      <code>set pump off</code><br>
      <code>set lamp2 on-till 19:59</code><br>
      <code>set lamp2 on-for-timer 00:02:30</code><br>
      <code>set lamp2 on-for-timer 150</code><br>
    </ul>
    <br>
    Notes:
    <ul>
      <li>Only switching and dimming are supported by now.</li>
      <li>Dimming is valid only for a dimmable device as specified by
          the <code>model</code> argument in its <code>define</code>
          statement.</li>
      <li>An X10 device has 210 discrete brightness levels. If you use a
          X10 sender, e.g. a remote control or a wall switch to dim, a
          brightness step is 100%/210.</li>
      <a id="X10-set-dimup" data-pattern="dim(up|down)"></a>
      <li><code>dimdown</code> and <code>dimup</code> take a number in the
          range from 0 to 22 as argument. It is assumed that argument 1 is
          a 1% brightness change (microdim) and arguments 2 to 22 are
          10%..100% brightness changes. The meaning of argument 0 is
          unclear.</li>
      <li>This currently leads to some confusion in the logs as the
          <code>dimdown</code> and <code>dimup</code> codes are logged with
          different meaning of the arguments depending on whether the commands
          were sent from the PC or from a remote control or a wall switch.</li>
      <li><code>dimdown</code> and <code>dimup</code> from on and off states may
          have unexpected results. This seems to be a feature of the X10
          devices.</li>
      <li><code>on-till</code> requires an absolute time in the "at" format
          (HH:MM[:SS]) or { &lt;perl code&gt; }, where the perl code
          returns a time specification).
          If the current time is greater than the specified time, then the
          command is ignored, else an "on" command is generated, and for the
          given "till-time" an off command is scheduleld via the at command.
          <code>off-till</code> invert the conditions above.</li>
      <li><code>on-till-overnight</code> or <code>off-till-overnight</code>
          the specified time is not compared to the current time.</li>
      <li><code>on-for-timer</code> or <code>off-for-timer</code> requires a
           relative time in the "at" format (HH:MM[:SS]) or { &lt;perl code&gt; },
           where the perl code returns a time specification) or seconds.</li>
    </ul>
  </ul>
  <br>

  <a id="X10-get"></a>
  <b>Get</b> <ul>N/A</ul><br>

  <a id="X10-attr"></a>
  <b>Attributes</b>
  <ul>
  <li><a href="#do_not_notify">do_not_notify</a></li>
    <li><a href="#attrdummy">dummy</a></li>
    <li><a href="#showtime">showtime</a></li>
    <li><a href="#model">model</a> (lm12,lm15,am12,tm13)</li>
    <li><a href="#IODev">IODev</a></li><br>
    <li><a href="#eventMap">eventMap</a></li><br>
  </ul>
  <br>
</ul>

=end html
=cut
