package RFC::TempControl;
#$Id: TempControl.pm,v 1.97 2025/01/28 09:33:15 sqko Exp $
use strict;
use warnings;
use RFC::Main qw();
use RFC::Header;
use RFC::BaseDevice;
use RFC::Cache;
use Eurotherm;
use RFC::OmronTemp;
our @ISA = qw(RFC::BaseDevice);

###############################################################################
#
#
#   Package RFC::TempControl
#
#
# This class defines the behaviour of temperature controlers for furnace
# control. To obtain aTempControl instance call one o fthe constructors
# like shown below:
#
# $id = RFC::TempControl->new($name,$rig)
#
# All TempControl objects have the following member functions in addition to those
# derived from RFC::BaseDevce and Debug.pm:
#
# $id->get_atemp()       returns the active (measured) temperature.
# $id->get_output()      returns the output power.
# $id->get_temp()        Returns the temperature setpoint.
# $id->get_ramp()        Returns the temperature ramp rate (C/hour).
# $id->get_offset()      Returns the offset between the temperature setpoint
#                        and the actual measured temperature.
#                        For a functiioning controler this should be close to
#                        0 but if the furnace can not heat suficiently (due to broken
#                        heating coils or similar) it will be positive.
# $id->set_temp($[$][$]) Sets the temperature setpoint.
#                        If an additional argument is specified, it is assumed to be
#                        the username of the controlling user and that name is appended
#                        to the string appended to the rigs proglog.
#                        an exception to this is if a further argument is specified in 
#                        which case no proglog entry is appended! 
# $id->set_ramp($[$][$]) Sets the temperature ramp rate (C/hour).
#                        If an additional argument is specified, it is assumed to be
#                        the username of the controlling user and that name is appended
#                        to the string appended to the rigs proglog.
#                        an exception to this is if a further argument is specified in 
#                        which case no proglog entry is appended! 
# $id->set($[$][$])        Alias for set_temp.
# $id->minramp()           Returns the minimum possible ramprate.
# $id->maxramp()           Returns the maximum allowed ramprate.
# $id->mintemp()           Returns the minimum allowed temperature.
# $id->maxtemp()           Returns the maximum allowed temperature.
# $id->check_ramp($[$][$]) Checks if the specified ramprate is within the allowed.
#                          range. If not it returns the minimum or maximum value allowed
#                          based on what is closest to the specified value.
#                          If a second (optional) argument is specified, it is assumed to 
#                          be the username of the user wanting to set the ramprate and a
#                          warning is issued to the log except if a third (optional) 
#                          argument is specified (similar to set_ramp()) 
#
###############################################################################




BEGIN {
    use Exporter ();
    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
    $VERSION = 1.0;
    %EXPORT_TAGS = (
        'standard'=>[],
                    );
    Exporter::export_ok_tags('standard');
    Exporter::export_tags('standard');
    @ISA = qw(Exporter);
}

sub new()
{
    my $class = shift;
    my $name = shift;
    my $rig = shift;
    my $self = $class->SUPER::new($name,$rig);
    $self->{'_html_template'} = 'Tempcontrol.tt';
    $self->{'_mode'} = 'Automatic';
    $self->{'_monitor_rows'} = '3,5,7';
    $self->{'_setup_tags'} = ['mode','controler_type','tty','address','controler_mode','type','maxramp','minramp','maxtemp','mintemp','show_plot','use_cache'];
    $self->{'_desc'} = {
	'mode' => 'Device control mode',
	'controler_type' => 'Device type',
	'tty' => 'Communication device',
	'address' => 'Device address',
	'controler_mode' => 'Number format',
	'type' => 'Communication mode',
	'title' => 'Optional device title',
	'maxtemp' => 'maximum allowed setpoint, not enforced if not set',
	'mintemp' => 'Minimum allowed setpoint, not enforced if not set',
	'maxramp' => 'Maximum allowable ramprate in C/hour (no value means no limit)',
	'minramp' => 'Minimum allowable ramprate in C/hour (Default 6 C/hour for Eurotherm)',
        'use_cache'=>'Determines if persisten caching of read values is allowed',
    };
    $self->{'_default'} = {
	'mode' => 'Automatic',
	'controler_type' => 'E2216',
	'tty' => 'ttyS0',
	'address' => 1,
	'controler_mode' => 'integer',
	'type' => 'modbus',
        'show_plot'=>'Yes',
        'use_cache'=>'No',
	'minramp' => 6,
    };
    $self->{'_readstring_furnace'} = 1;
    $self->{'_has_ramp'} = 1;
    $self->{'_set_fn_names'} = ['set_tenp','set_ramp']; # List of function names which can trigger a notify event
    $self->{'_values_mode'} = ['Manual','Automatic','Readonly'];
    $self->{'_values_controler_type'} = [(RFC::Device::list_TEMP())];
    $self->{'_values_controler_mode'} = ['integer','decimal'];
    $self->{'_values_type'} = ['modbus','bisynch'];
    $self->{'_section'} = 'TControl_'.$self->name;
    $self->{'_section'} = 'temperature' if (!$self->rig->config_section_exists($self->sectionname) && $self->name eq 'temperature');
    foreach my $tag (@{$self->{'_setup_tags'}})
    {
	$self->{'_'.$tag} = $self->get_conf($tag);
        $self->{'_'.$tag} = $self->get_default($tag) unless (defined($self->{'_'.$tag}));
    }
    $self->{'_controler_type'} = shift if (@_);
    $self->{'_communication_type'} = $self->{'_type'};
    my $dev = Eurotherm::new_controler($self->{'_controler_type'},$self->{'_tty'},$self->{'_address'},$self->{'_type'},$self->{'_controler_mode'});
    $dev = RFC::OmronTemp::new_controler($self->{'_controler_type'},$self->{'_tty'},$self->{'_address'}) unless ($dev);
    $dev = RFC::CalTemp::new_controler($self->{'_controler_type'},$self->{'_tty'},$self->{'_address'}) unless ($dev);
    if ($self->{'_controler_type'} =~ /W/)
    {
	$self->{'_setup_tags'} = ['mode','controler_type','tty','address'];
	$self->{'_values_controler_mode'} = ['integer'];
	$self->{'_values_type'} = ['modbus'];	
    }
    if ($self->{'_controler_type'} =~ /Omron|Cal/)
    {
	$self->{'_setup_tags'} = ['mode','controler_type','type','tty','address'];
	$self->{'_values_controler_mode'} = ['integer'];
	$self->{'_values_type'} = ['modbus'];	
    }
    $self->{'_device'} = $dev;
    $self->die("No device returned") unless $dev;
    $self->{'_type'} = $self->{'_controler_type'};
    $self->{'_read_names'} = ['read','get_atemp','get_output','get_ramp','get_temp','get_offset'];
    if ($self->{'_controler_type'} =~ /Omron|Cal/)
    {
	$self->{'_read_names'} = ['read','get_atemp','get_ramp','get_temp','get_offset'];
    }
    $self->{'_set_names'} = ['set_ramp','set_temp','set'];
    $self->{'_readonly'} = 1 if ($self->mode =~ /readonly/i);
    return $self;
};

sub debug()
{
    my $self = shift;
    $self->SUPER::debug(@_);
    if (@_)
    {
        $self->{'_device'}->debug($self->{'_debug'}) unless (ref($self->{'_device'}) =~ /e_\d+|West\d+|Linkam/);
    }
    return $self->{'_debug'};
}

sub readstring()
{
    my $self = shift;
    return undef if ($self->readstring_ignore);
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug()) if ($self->debug);
    }
    $self->check_cache($data);
    my $val = $self->read($data,@_);
    $val = -32768 unless (defined($val) && $val =~ /\d/);
    my $aset = $self->get_atemp($data,@_);
    $aset = -32768 unless (defined($aset) && $aset =~ /\d/);
    my $pow = -32768;
    unless ($self->{'_controler_type'} =~ /Omron|Cal/)
    {
	$pow = $self->get_output($data);
	$pow = -32768 unless (defined($pow) && $pow =~ /\d/);
    }
    my @t = $self->get_time();
    my $t = join(':',@t);
    my $res = join(' ',$self->name.'_setpoint',$t,time,$val,$self->name.'_temp',$t,time,$aset,$self->name.'_output',$t,time,$pow);
    $self->writedebug($res) if ($self->debug > 1);
    return $res;
}

sub get_offset
{
    my $self = shift;
    my $sp = $self->read(@_);
    my $at = $self->get_atemp(@_);
    my $diff = $sp - $at;
    return $diff;
}

sub print_config()
{
    my $self = shift;
    my @arr = ();
    push @arr,'Name: '.$self->name;
    push @arr,'Type: '.$self->type;
    push @arr,'Mode: '.$self->mode;
    push @arr,'Tty: '.$self->{'_tty'};
    push @arr,'Address: '.$self->{'_address'};
    push @arr,'Communication: '.$self->{'_controler_mode'};
    push @arr,'Communication mode: '.$self->{'_communication_type'};
    return join(', ',@arr);    
}

sub read_nocache
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = $_[0];
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug()) if ($self->debug);
    }
    $self->check_cache($data);
    my $cached = $data->get($self->{'_tty'},$self->{'_address'},'temp');
    return $cached if (defined($cached));
    my $temp = undef;
    eval {
	$temp = $self->{'_device'}->get_active_setpoint(@_);
	unless (defined($temp))
	{
	    select(undef,undef,undef,0.5);
	    $temp = $self->{'_device'}->get_active_setpoint(@_) 
	}
	$temp = $self->{'_device'}->get_active_temp(@_) unless (defined($temp) && $temp ne '');
	unless (defined($temp))
	{
	    select(undef,undef,undef,0.5);
	    $temp = $self->{'_device'}->get_active_temp(@_) 
	}
	die "Could not get temperature for $self\n" unless (defined($temp) && $temp ne '');
    };
    if ($@)
    {
	my $message = "Error: $@";
	$self->rig->warning_mail($self,$message,'Error for '.$self->rig);
    }
    $temp = -32768 unless defined($temp);
    $data->set($self->{'_tty'},$self->{'_address'},'temp',$temp);
    $self->writedebug("Undefiend temperature returned by controler device $self") if ($self->debug && (!defined($temp) || $temp !~ /\d+/));
    $self->set_cache($temp);
    $self->{'_last_read'} = Time::HiRes::time;
    return $temp;
}

sub get_atemp
{
    my $self = shift;
    if ($self->{'_use_cache'} && $self->{'_use_cache'} eq 'Yes')
    {
        my $val = $self->rig->get_cache($self->type,$self->name.'_atemp'); 
        return $val if (defined($val));
    }
    my $data = undef;
    if (@_)
    {
        $data = shift;
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug()) if ($self->debug);
    }
    $self->check_cache($data);
    my $cached = $data->get($self->{'_tty'},$self->{'_address'},'atemp');
    return $cached if (defined($cached));
    my $temp = undef;
    eval {
	$temp = $self->{'_device'}->get_active_temp($data);
	unless (defined($temp) && $temp =~ /\d+/)
	{
	    select(undef,undef,undef,0.5);
	    $temp = $self->{'_device'}->get_active_temp($data);
	}
	die "Undefined active temperature returned by device ".ref($self->{'_device'}). "\n" unless (defined($temp) && $temp =~ /\d+/);
    };
    if ($@)
    {
	my $message = "Warning: $@";
	$self->rig->warning_mail($self,$message,'Warning for '.$self->rig);
	$temp = undef;
    }
    $data->set($self->{'_tty'},$self->{'_address'},'atemp',$temp);
    $self->writedebug("Undefiend active temperature returned by controler device $self") if ($self->debug && (!defined($temp) || $temp !~ /\d+/));
    $self->rig->set_cache($self->type,$self->name.'_atemp',$temp);
    return $temp;
}


sub get_output
{
    my $self = shift;
    if ($self->{'_use_cache'} && $self->{'_use_cache'} eq 'Yes')
    {
        my $val = $self->rig->get_cache($self->type,$self->name.'output'); 
        return $val if (defined($val));
    }
    my $data = undef;
    if (@_)
    {
        $data = shift;
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug()) if ($self->debug);
    }
    $self->check_cache($data);
    my $cached = $data->get($self->{'_tty'},$self->{'_address'},'output');
    return $cached if (defined($cached));
    my $temp = undef;
    eval {
	$temp = $self->{'_device'}->get_power($data);
	unless (defined($temp))
	{
	    select(undef,undef,undef,0.5);
	    $temp = $self->{'_device'}->get_power($data); 
	}
	die "Undefined output power returned by device ".ref($self->{'_device'}). "\n" unless (defined($temp) && $temp ne '');
    };
    if ($@)
    {
	my $message = "Warning: $@";
	$self->rig->warning_mail($self,$message,'Warning for '.$self->rig);
	$temp = undef;
    }
    $data->set($self->{'_tty'},$self->{'_address'},'output',$temp);
    $self->writedebug("Undefiend output power returned by controler device $self") if ($self->debug && (!defined($temp) || $temp !~ /\d+/));
    $self->rig->set_cache($self->type,$self->name.'output',$temp);
    return $temp;
}




sub monitor
{
    my $self = shift;
    my $data = undef;
    if (@_)
    {
        $data = shift;
    }
    else
    {
        $data = RFC::Cache->new();
        $data->debug($self->debug()) if ($self->debug);
    }
    $self->check_cache($data);
    my ($val,$aset,$pow) = (-32768,-32768,-32768);
    $val = $self->read($data);
    eval{
	$aset = $self->{'_device'}->get_active_temp();
	unless(defined($aset))
	{
	    select(undef,undef,undef,0.5);
	    $aset = $self->{'_device'}->get_active_temp();
	}
	die "Undefined active temperature returned by device ".ref($self->{'_device'})."\n" unless (defined($aset) && $aset ne '');
	unless ($self->{'_controler_type'} =~ /Omron|Cal/)
	{
	    $pow = $self->get_output($data);
	    die "Undefined output power returned by device ".ref($self->{'_device'})."\n" unless (defined($pow) && $pow ne '');
	}
    };
    if ($@)
    {
	$self->writedebug("Warning for $self: $@");
	my $message = "Warning: $@";
	$self->rig->warning_mail($self,$message,'Warning for '.$self->rig);
    }
    foreach ($val,$aset,$pow)
    {
	$_ = -32768 unless (defined($_) && /\d/);
    }
    return join(' ','Setpoint',$val,'Active_temp',$aset,'Output',$pow);
}

sub get_temp
{
    my $self = shift;
    my $res = undef;
    eval {
	$res = $self->{'_device'}->get_temp;
	unless (defined($res))
	{
	    select(undef,undef,undef,0.5);
	    $res = $self->{'_device'}->get_temp;
	}
	die "Undefined setpoint returned by device ".ref($self->{'_device'})."\n" unless (defined($res) && $res ne '');
    };
    if ($@)
    {
	$self->writedebug("Warning for $self: $@");
	my $message = "Warning: $self - $@";
	$self->rig->warning_mail($self,$message,'Warning for '.$self->rig);
    }
    return $res;
}

sub get_ramp
{
    my $self = shift;
    my $res = undef;
    eval {
	$res = $self->{'_device'}->get_ramp;
	unless (defined($res))
	{
	    select(undef,undef,undef,0.5);
	    $res = $self->{'_device'}->get_ramp;
	}
	die "Undefined ramprate returned by device ".ref($self->{'_device'})." on ".$self->rig."\n" unless (defined($res) && $res ne '');
    };
    if ($@)
    {
	$self->writedebug("Warning for $self: $@");
	my $message = "Warning: $self - $@";
	$self->rig->warning_mail($self,$message);
    }
    return $res;
}

sub minramp()
{
    my $self = shift;
    my $min = 0;
    $min = $self->{'_minramp'} if (defined($self->{'_minramp'}) && $self->{'_minramp'});
    return $min;
}

sub maxramp()
{
    my $self = shift;
    my $max = 1e100;
    $max = $self->{'_maxramp'} if (defined($self->{'_maxramp'}) && $self->{'_maxramp'});
    return $max;
}

sub mintemp()
{
    my $self = shift;
    my $min = -273; ## Well no need to break the universe...
    $min = $self->{'_mintemp'} if (defined($self->{'_mintemp'}) && $self->{'_mintemp'});
    return $min;
}

sub maxtemp()
{
    my $self = shift;
    my $max = 6000; ## No need to go higher than the surface of the sun...
    $max = $self->{'_maxtemp'} if (defined($self->{'_maxtemp'}) && $self->{'_maxtemp'});
    return $max;
}

sub check_ramp
{
    my $self = shift;
    my $ramp = shift;
    my $user = shift;
    if ($ramp > $self->maxramp())
    {
	$ramp = $self->maxramp();
	unless (@_)
	{
	    if ($user)
	    {
		$self->rig->errorlog("Warning: Temperature ramp for $self was to be set above maximum ($ramp C/hour)!",$user);
	    }
	    else
	    {
		$self->rig->errorlog("Warning: Temperature ramp for $self was to be set above maximum ($ramp C/hour)!");
	    }
	}
    }
    if ($ramp < $self->minramp())
    {
	$ramp = $self->minramp();
	unless (@_)
	{
	    if ($user)
	    {
		$self->rig->errorlog("Warning: Temperature ramp for $self was to be set below minimum ($ramp C/hour)!",$user);
	    }
	    else
	    {
		$self->rig->errorlog("Warning: Temperature ramp for $self was to be set below minimum ($ramp C/hour)!");
	    }
	}
    }
    return $ramp;
}

sub set_ramp
{
    my $self = shift;
    my $ramp = $self->check_ramp(shift);
    my $ignore = $self->rig->disable_warning_mails();
    $self->rig->disable_warning_mails(0);
    if ($self->mode =~ /auto|read/i)
    {
	my $res = undef;
	eval {
	    $res = $self->{'_device'}->set_ramp($ramp);
	};
	if ($@)
	{
	    $self->writedebug("Error setting ramp for $self: $@");
	    my $message = "Error setting ramp for $self on rig: $@";
	    $self->rig->warning_mail($self,$message,'Error for '.$self->rig);
	    $self->rig->disable_warning_mails($ignore);
	    return undef;
	}
	$self->proglog("Temperature ramp for $self set to $ramp C/hour",@_);
    }
    else
    {
	$self->proglog("Temperature ramp for $self set manually to $ramp C/hour",@_);
    }
    $self->notify('set_ramp',$ramp,@_);
    $self->rig->unset_warning($self);
    $self->rig->disable_warning_mails($ignore);
    return $ramp;
}

sub set_temp()
{
    my $self = shift;
    my $temp = shift;
    my $min = $self->{'_mintemp'};
    my $max = $self->{'_maxtemp'};
    my $ignore = $self->rig->disable_warning_mails();
    $self->rig->disable_warning_mails(0);
    if (defined($max) && $temp > $max)
    {
	$temp = $max;
	$self->proglog("Temperature was to be set above maximum of $max C",@_);
    }
    if (defined($min) && $temp < $min)
    {
	$temp = $min;
	$self->proglog("Temperature was to be set belov minimum of $min C",@_);
    }
    if ($self->mode =~ /auto|read/i)
    {
	my $res = undef;
	eval {
	    $res = $self->{'_device'}->set_temp($temp);
	};
	if ($@)
	{
	    $self->writedebug("Error setting temperature for $self: $@");
	    my $message = "Error setting temperature for $self: $@";
	    $self->rig->warning_mail($self,$message,'Error for '.$self->rig);
	    $self->rig->disable_warning_mails($ignore);
	    return undef;
	}	
	$self->proglog("Temperature for $self set to $temp C",@_);
    }
    else
    {
	$self->proglog("Temperature for $self set manually to $temp C",@_);
    }
    $self->notify('set_temp',$temp,@_);
    $self->rig->unset_warning($self);
    $self->rig->disable_warning_mails($ignore);
    return $temp;
}

sub set
{
    my $self = shift;
    return $self->set_temp(@_);
}

sub test()
{
    my $self = shift;
    my $res = $self->{'_device'}->get_temp();
    return 1 if (defined($res) && $res =~ /\d/ && ($res !~ /refused/i));
    return 0;
}

sub test_string()
{
    my $self = shift;
    my $temp = $self->{'_device'}->get_temp();
    chomp $temp;
    my $ramp = $self->{'_device'}->get_ramp();
    chomp $ramp;
    return "Temperature controler $self, Setpoint: $temp C, ramprate $ramp C/hour";
}
