diff --git a/fhem/FHEM/52_I2C_ADS1x1x.pm b/fhem/FHEM/52_I2C_ADS1x1x.pm new file mode 100755 index 000000000..1f004e084 --- /dev/null +++ b/fhem/FHEM/52_I2C_ADS1x1x.pm @@ -0,0 +1,752 @@ +############################################## +# $Id$ +# Credits: +# Texas Instruments for the Chip - Documentation at https://www.ti.com/lit/ds/sbas444d/sbas444d.pdf (ADS111x) +# and https://www.ti.com/lit/ds/sbas473e/sbas473e.pdf (ADS101x) +# Karsten Grüttner - for the initial ADS1x1x implementation +# Klaus Wittstock: for the PCF8574 module that I used as a basis for this revised ADS1x1x implementation +# +# +package main; + +use strict; +use warnings; +use SetExtensions; +use Scalar::Util qw(looks_like_number); + +my %I2C_ADS1x1x_Config = +( + + 'State' => # Bit [15] + { + 'SINGLE' => 1 << 15, # Write: Begin a single conversion (when in power-down mode) + 'BUSY' => 0, # Read: Bit = 0 Device is currently performing a conversion + 'NOT_BUSY' => 1 << 15 # Read: Bit = 1 Device is not currently performing a conversion + }, + 'Mux' => # Bits [14:12] + { + 'COMP_0_1' => 0 , # AINP = AIN0 and AINN = AIN1 , default + 'COMP_0_3' => 1 << 12, # AINP = AIN0 and AINN = AIN3 + 'COMP_1_3' => 2 << 12 , # AINP = AIN1 and AINN = AIN3 + 'COMP_2_3' => 3 << 12 , # AINP = AIN2 and AINN = AIN3 + 'SINGLE_0' => 4 << 12 , # AINP = AIN0 and AINN = GND + 'SINGLE_1' => 5 << 12 , # AINP = AIN1 and AINN = GND + 'SINGLE_2' => 6 << 12 , # AINP = AIN2 and AINN = GND + 'SINGLE_3' => 7 << 12 # AINP = AIN3 and AINN = GND + }, + 'Gain' => # Bits [11:9] + { + '6V' => { code => 0, refVoltage => 6.144 }, + '4V' => { code => 1 << 9, refVoltage => 4.096 }, # default + '2V' => { code => 2 << 9, refVoltage => 2.048 }, + '1V' => { code => 3 << 9, refVoltage => 1.024 }, + '0.5V' => { code => 4 << 9, refVoltage => 0.512 }, + '0.25V' => { code => 5 << 9, refVoltage => 0.256 } + + }, + 'Data_Rate' => # Bits [7:5] "delay" refers to ADS111x only, but not used at all currently + { + '1/16x' => { code => 0, delay => 1.0/8 }, + '1/8x' => { code => 1 << 5, delay => 1.0/16 }, + '1/4x' => { code => 2 << 5, delay => 1.0/32 }, + '1/2x' => { code => 3 << 5, delay => 1.0/64 }, + '1x' => { code => 4 << 5, delay => 1.0/128 }, # default + '2x' => { code => 5 << 5, delay => 1.0/250 }, + '4x' => { code => 6 << 5, delay => 1.0/475 }, + '8x' => { code => 7 << 5, delay => 1.0/860 } + }, + 'Operation_Mode' => # Bit [8] + { + 'Continuously' => 0, # einmalig initialisiert, kann immer gelesen werden, geeignet für Dauerüberwachung + 'SingleShot' => 1 << 8 # wacht zum einmaligen Lesen auf und legt sich wieder schlafen, geeignet für Messungen mit großen Pausen dazwischen + }, + + 'Comparator_Mode' => # Bit [4] + { + 'Traditional' => 0, + 'Window' => 1 << 4 + }, + 'Comparator_Polarity' => # Bit [3] + { + 'ActiveLow' => 0, # default + 'ActiveHigh' => 1 << 3 + + }, + 'Latching_Comparator' => # Bit [2] + { + 'off' => 0, , # default + 'on' => 1 << 2 + }, + 'Comparator_Queue_Disable' => # Bits [1:0] + { + 'AfterOneConversion' => 0, + 'AfterTwoConversions' => 1, + 'AfterFourConversions' => 2, + 'disable' => 3 # default + } + +); + +sub I2C_ADS1x1x_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = "I2C_ADS1x1x_Define"; + $hash->{InitFn} = 'I2C_ADS1x1x_Init'; + $hash->{AttrFn} = "I2C_ADS1x1x_Attr"; + $hash->{SetFn} = "I2C_ADS1x1x_Set"; + $hash->{StateFn} = "I2C_ADS1x1x_State"; + $hash->{GetFn} = "I2C_ADS1x1x_Get"; + $hash->{UndefFn} = "I2C_ADS1x1x_Undef"; + $hash->{I2CRecFn} = "I2C_ADS1x1x_I2CRec"; + $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:1,0 showtime:1,0 ". + "a0_mode:RTD,NTC,RAW,RES,off ". + "a1_mode:RTD,NTC,RAW,RES,off ". + "a2_mode:RTD,NTC,RAW,RES,off ". + "a3_mode:RTD,NTC,RAW,RES,off ". + "a0_res a1_res a2_res a3_res ". + "a0_r0 a1_r0 a2_r0 a3_r0 ". + "a0_bval a1_bval a2_bval a3_bval ". + "a0_gain:6V,4V,2V,1V,0.5V,0.25V ". + "a1_gain:6V,4V,2V,1V,0.5V,0.25V ". + "a2_gain:6V,4V,2V,1V,0.5V,0.25V ". + "a3_gain:6V,4V,2V,1V,0.5V,0.25V ". + "decimals:0,1,2,3,4,5 ". + "sys_voltage " . + "data_rate:1/16x,1/8x,1/4x,1/2x,1x,2x,4x,8x ". + "mux:SINGLE ". + "device:ADS1013,ADS1014,ADS1015,ADS1113,ADS1114,ADS1115 ". + #"comparator_polarity:ActiveLow,ActiveHigh ". + #"operation_mode:SingleShot,Continuously ". ### Does not make sense with multiple inputs + #"comparator_mode:Traditional,Window ". + #"latching_comparator:on,off ". + #"comparator_queue_disable:AfterOneConversion,AfterTwoConversion,AfterFourConversion,disable ". + "poll_interval ". + "poll_interleave ". + "$readingFnAttributes"; +} +################################### Todo: Set or Attribute for Mode? Other sets needed? +sub I2C_ADS1x1x_Set($@) { # + my ($hash, @a) = @_; + my $name =$a[0]; + my $cmd = $a[1]; + my $val = $a[2]; + + if ( $cmd && $cmd eq "Update") { + #Make sure there is no reading cycle running and re-start polling (which starts with an inital read) + RemoveInternalTimer($hash) if ( defined (AttrVal($hash->{NAME}, "poll_interval", undef)) ); + $hash->{helper}{state}=0; #Reset state machine + InternalTimer(gettimeofday() + 1, 'I2C_ADS1x1x_Execute', $hash, 0); + return undef; + } else { + my $list = "Update:noArg"; + return "Unknown argument $a[1], choose one of " . $list if defined $list; + return "Unknown argument $a[1]"; + } + if (!defined $hash->{IODev}) { + readingsSingleUpdate($hash, 'state', 'No IODev defined',0); + return "$name: no IO device defined"; + } + return undef; +} +################################### +sub I2C_ADS1x1x_Get($@) { + #Nothing to be done here, let all updates run asychroniously with timers + return undef; +} + +sub I2C_ADS1x1x_Execute($@) { + my ($hash) = @_; + my $state=$hash->{helper}{state}; + my $channels=$hash->{helper}{channels}; + #Default time between reading channels + my $nexttimer=AttrVal($hash->{NAME}, 'poll_interleave', 0.008); + my $interleave=$nexttimer; + if (!defined($state)) {$state=0}; + if ($state%2 == 0) {$nexttimer=0.008;} #8 ms conversiontime for even numbers + if ($state<($channels*2-1)) { + $hash->{helper}{state}+=1; + } else { + $hash->{helper}{state}=0; + #Interleave to next complete read cycle is poll interval + $nexttimer = AttrVal($hash->{NAME}, 'poll_interval', 5)*60 - $channels*(0.008+$interleave); #Substract channel timers to have more or less constant interval + } + Log3 $hash->{NAME}, 5, $hash->{NAME}." => Processing state $state timer $nexttimer channels: $channels newstate:".$hash->{helper}{state}; + if (!defined AttrVal($hash->{NAME}, "IODev", undef)) {return;} + if ($state==0) { + I2C_ADS1x1x_InitConfig($hash,0); + } elsif ($state==1) { + I2C_ADS1x1x_ReadData($hash,0); + } elsif ($state==2) { + I2C_ADS1x1x_InitConfig($hash,1); + } elsif ($state==3) { + I2C_ADS1x1x_ReadData($hash,1); + } elsif ($state==4) { + I2C_ADS1x1x_InitConfig($hash,2); + } elsif ($state==5) { + I2C_ADS1x1x_ReadData($hash,2); + } elsif ($state==6) { + I2C_ADS1x1x_InitConfig($hash,3); + } elsif ($state==7) { + I2C_ADS1x1x_ReadData($hash,3); + } + + #Initalize next Timer for Reading Results in 8ms (time required for conversion to be ready) + InternalTimer(gettimeofday()+$nexttimer, \&I2C_ADS1x1x_Execute, $hash,0) unless $nexttimer<=0; + return undef; +} + +sub I2C_ADS1x1x_InitConfig(@) { + my ($hash, $sensor) = @_; + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + my $mux=AttrVal($hash->{NAME}, "mux", "SINGLE"); + return undef if ($mux ne "SINGLE"); #Only SINGLE mode supported + + my $mode=AttrVal($hash->{NAME}, "a".$sensor."_mode", "RAW"); + + if ($mode ne "off") { + my $sensval=$mux."_".$sensor; + my $gain=AttrVal($hash->{NAME}, "a".$sensor."_gain", "4V"); + my $config = $hash->{helper}{configword}| + $I2C_ADS1x1x_Config{'Mux'}{$sensval}| + $I2C_ADS1x1x_Config{'Gain'}{$gain}{code}; + + my $low_byte = $config & 0xff; + my $high_byte = ($config & 0xff00) >> 8; + my %sendpackage = ( i2caddress => $hash->{I2C_Address}, direction => "i2cwrite", reg=> 1, sensor=>$sensor, data => $high_byte. " " .$low_byte); + Log3 $hash->{NAME}, 4, $hash->{NAME}." => $pname CONFIG adr:".$hash->{I2C_Address}." Sensor $sensor Byte0:$high_byte Byte1:$low_byte"; + CallFn($pname, "I2CWrtFn", $phash, \%sendpackage); + } +} + +sub I2C_ADS1x1x_ReadData(@) { + my ($hash, $sensor) = @_; + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + #Gain needs to be passed through for calculation + my $gain=AttrVal($hash->{NAME}, "a".$sensor."_gain", "4V"); + my %sendpackage = ( i2caddress => $hash->{I2C_Address}, direction => "i2cread", reg=> 0, sensor=>$sensor, gain=>$gain, nbyte => 2); + Log3 $hash->{NAME}, 5, $hash->{NAME}." => $pname READ adr:".$hash->{I2C_Address}." Sensor $sensor Gain $gain"; + CallFn($pname, "I2CWrtFn", $phash, \%sendpackage); +} + +################################### +sub I2C_ADS1x1x_Attr(@) { # + my ($command, $name, $attr, $val) = @_; + my $hash = $defs{$name}; + my $msg = undef; + if ($command && $command eq "set" && $attr && $attr eq "IODev") { + if ($main::init_done and (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $val)) { + main::AssignIoPort($hash,$val); + my @def = split (' ',$hash->{DEF}); + I2C_ADS1x1x_Init($hash,\@def) if (defined ($hash->{IODev})); + } + } + if ($attr eq 'poll_interval') { + if ( defined($val) ) { + if ( looks_like_number($val) && $val >= 0) { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+1, 'I2C_ADS1x1x_Execute', $hash, 0) if $val>0; + } else { + $msg = "$hash->{NAME}: Wrong poll intervall defined. poll_interval must be a number >= 0"; + } + } else { + RemoveInternalTimer($hash); + } + } elsif ($attr eq 'device') { + my $channels=1; + if (!defined $val or $val =~ m/^ADS1[0|1]15$/i ) { + $channels=4; # Only these two devices have 4 channels + } + $hash->{helper}{channels}=$channels; + } + + #check for correct values while setting so we need no error handling later + foreach ('sys_voltage','a0_res','a1_res','a2_res','a3_res', 'a0_r0', 'a1_r0', 'a2_r0', 'a3_r0', 'a0_bval', 'a1_bval', 'a2_bval', 'a3_bval') { + if ($attr eq $_) { + if ( defined($val) ) { + if ( !looks_like_number($val) || $val <= 0) { + $msg = "$hash->{NAME}: ".$attr." must be a number > 0"; + } + } + } + } + I2C_ADS1x1x_Prepare($hash); #Update predefined variables so any attribute changes are reflected + return $msg; +} +################################### +sub I2C_ADS1x1x_Define($$) { # + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + if ($main::init_done) { + eval { I2C_ADS1x1x_Init( $hash, [ @a[ 2 .. scalar(@a) - 1 ] ] ); }; + return I2C_ADS1x1x_Catch($@) if $@; + } + return undef; +} +################################### +sub I2C_ADS1x1x_Init($$) { # + my ( $hash, $args ) = @_; + #my @a = split("[ \t]+", $args); + my $name = $hash->{NAME}; + if (defined $args && int(@$args) != 1) { + return "Define: Wrong syntax. Usage:\n" . + "define I2C_ADS1x1x "; + } + if (defined (my $address = shift @$args)) { + $hash->{I2C_Address} = $address =~ /^0.*$/ ? oct($address) : $address; + } else { + readingsSingleUpdate($hash, 'state', 'Invalid I2C Adress',0); + return "$name I2C Address not valid"; + } + AssignIoPort($hash); + readingsSingleUpdate($hash, 'state', 'Initialized',0); + I2C_ADS1x1x_Set($hash, $name, "setfromreading"); + I2C_ADS1x1x_Prepare($hash); + RemoveInternalTimer($hash); + my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 5)*60; + InternalTimer(gettimeofday() + $pollInterval, 'I2C_ADS1x1x_Execute', $hash, 0) if ($pollInterval > 0); + return; +} + +sub I2C_ADS1x1x_Prepare($) { + my ($hash)=@_; + $hash->{helper}{state}=0; #initalize state machine + $hash->{helper}{channels}=4; #for default ADS1115, will be overwritten with different ATTR setting + my $mux=AttrVal($hash->{NAME}, "mux", "SINGLE"); + my $device=AttrVal($hash->{NAME}, "device", "ADS1115"); + my $rate=AttrVal($hash->{NAME}, "data_rate", "1x"); + my $opmode=AttrVal($hash->{NAME}, "operation_mode", "SingleShot"); + my $cmode=AttrVal($hash->{NAME}, "comparator_mode", "Traditional"); + my $lcomp=AttrVal($hash->{NAME}, "latching_comparator", "on"); + my $cqueue=AttrVal($hash->{NAME}, "comparator_queue_disable", "AfterOneConversion"); + my $cpol=AttrVal($hash->{NAME}, "comparator_polarity", "ActiveLow"); + my $config = $I2C_ADS1x1x_Config{'State'}{SINGLE}| + $I2C_ADS1x1x_Config{'Data_Rate'}{$rate}{code}| + $I2C_ADS1x1x_Config{'Operation_Mode'}{$opmode}| + $I2C_ADS1x1x_Config{'Comparator_Mode'}{$cmode}| + $I2C_ADS1x1x_Config{'Latching_Comparator'}{$lcomp}| + $I2C_ADS1x1x_Config{'Comparator_Queue_Disable'}{$cqueue}| + $I2C_ADS1x1x_Config{'Comparator_Polarity'}{$cpol}; + $hash->{helper}{configword}=$config; +} + +################################### +sub I2C_ADS1x1x_Catch($) { + my $exception = shift; + if ($exception) { + $exception =~ /^(.*)( at.*FHEM.*)$/; + return $1; + } + return undef; +} +################################### +sub I2C_ADS1x1x_State($$$$) { #reload readings at FHEM start + my ($hash, $tim, $sname, $sval) = @_; + #No persistant data needed, using only attributes + return undef; +} +################################### +sub I2C_ADS1x1x_Undef($$) { # + my ($hash, $name) = @_; + RemoveInternalTimer($hash) if ( defined (AttrVal($hash->{NAME}, "poll_interval", undef)) ); + return undef; +} + +# Calculate temperature for PT1000/PT100 platinum temperature sensors +# ax_r0 = Resistance in Ohm at zero degrees C +sub I2C_ADS1x1x_RTD($@) { + my ($resistance,$sensor,$r0) = @_; + #my $aa=0.003851; #Deutscher Standard? + my $aa=0.0039083; #ITU-90 Standard + my $bb=-5.05E-08; #my own value + #my $bb=-5.7750E-07; #ITU-90 Standard + my $temperature=0; + my $root = $aa*$aa*$r0*$r0-4*$bb*$r0*($r0-$resistance); + if ($root>=0) { + $temperature=(-$aa*$r0+sqrt($root))/(2*$bb*$r0); + } + return $temperature; +} + +# Calculate temperature for NTC Sensors +# ax_r0 = Resistance in Ohm at 25 degrees C (typically 50K) +# ax_b = B-Value according to datasheet (for 50K often 3950) +sub I2C_ADS1x1x_NTC($@) { + my ($resistance,$sensor,$r0,$bval) = @_; + if ($resistance<0) {return 0;} # Prevent issue in error case + my $steinhart; + $steinhart = $resistance / $r0; # (R/Ro) + $steinhart = log($steinhart); # ln(R/Ro) + $steinhart = $steinhart/$bval; # 1/B * ln(R/Ro) + $steinhart = $steinhart+ 1.0 / (25.0 + 273.15); # + (1/To) + $steinhart = 1.0 / $steinhart; # Invert + $steinhart = $steinhart-273.15; # convert to C + return $steinhart; +} + +################################### + +sub I2C_ADS1x1x_I2CRec($@) { # ueber CallFn vom physical aufgerufen + my ($hash, $clientmsg) = @_; + my $name = $hash->{NAME}; + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + my $clientHash = $defs{$name}; + my $msg = ""; + while ( my ( $k, $v ) = each %$clientmsg ) { #erzeugen von Internals fuer alle Keys in $clientmsg die mit dem physical Namen beginnen + $hash->{$k} = $v if $k =~ /^$pname/ ; + $msg = $msg . " $k=$v"; + } + Log3 $hash,5 , "$name: I2C reply:$msg"; + my $sval; + if ($clientmsg->{direction} && $clientmsg->{$pname . "_SENDSTAT"} && $clientmsg->{$pname . "_SENDSTAT"} eq "Ok") { + readingsBeginUpdate($hash); + if ($clientmsg->{direction} eq "i2cread" && defined($clientmsg->{received})) { + my ($high,$low) = split(/ /, $clientmsg->{received}); + my $value= $high<<8|$low; + Log3 $hash,5 , "$name:value:$value"; + my $gain=$clientmsg->{gain}; + my $refvoltage=$I2C_ADS1x1x_Config{'Gain'}{$gain}{refVoltage}; + + my $device=AttrVal($hash->{NAME}, "device", "ADS1115"); + my $mask=0x7fff; + my $bits=16; + #No differentiation for 12bit since those devices still submit 16bits with the 4 lower bits set to zero + my $voltage = ($value & $mask) * # filtere Bit 2^15 (0x8000) raus, das ist Vorzeichenmerkmal + ( $refvoltage/$mask) * # normiere anhand der Auflösung 2^15 im positiven Bereich + ( 1.0 - (2.0 * (($value & ($mask+1)) >> ($bits-1)))); # bei gesetzten Bit 2^15 Faktor -1, ansonsten +1 ($mask+1 = 0x8000/0x800) + + #rounded voltage only for reading, continue calculation will full precision + my $voltager = sprintf( '%.' . AttrVal($clientHash->{NAME}, 'decimals', 3) . 'f', $voltage ); + Log3 $hash,5 , "$name:voltage=$voltage, ref=".$I2C_ADS1x1x_Config{'Gain'}{$gain}{refVoltage}; + my $sensor= $clientmsg->{sensor}; + readingsBulkUpdate($hash, "a".$sensor."_voltage", $voltager) if (ReadingsVal($name,"a".$sensor."_voltage",0) != $voltager); + my $divider=AttrVal($name,"a".$sensor."_res",1000); + my $highvoltage=AttrVal($name,"sys_voltage",3.3); + #Always calculate resistance but only write to reading in case of "RES" mode + my $resistance=$divider*$voltage/($highvoltage-$voltage); + my $resistancer = sprintf( '%.' . AttrVal($name, 'decimals', 3) . 'f', $resistance ); + my $temperature=0; + my $mode=AttrVal($name,"a".$sensor."_mode",""); + Log3 $hash,5 , "$name:resistance=$resistance, with divider=$divider system_voltage=$highvoltage"; + if ($mode eq "RES") { + readingsBulkUpdate($hash, "a".$sensor."_resistance", $resistancer) if (ReadingsVal($name,"a".$sensor."_resistance",0) != $resistancer); + } elsif ($mode eq "RTD") { + $temperature=sprintf( '%.1f', I2C_ADS1x1x_RTD($resistance,$sensor,AttrVal($name,"a".$sensor."_r0",1000.0))); + Log3 $hash,5 , "$name:RTD Temp=$temperature °C"; + readingsBulkUpdate($hash, "a".$sensor."_temperature", $temperature) if (ReadingsVal($name,"a".$sensor."_temperature",0) != $temperature); + } elsif ($mode eq "NTC") { + $temperature=sprintf( '%.1f', I2C_ADS1x1x_NTC($resistance,$sensor,AttrVal($name,"a".$sensor."_res",50000.0),AttrVal($name,"a".$sensor."_b",3950.0))); + Log3 $hash,5 , "$name:NTC Temp=$temperature °C"; + readingsBulkUpdate($hash, "a".$sensor."_temperature", $temperature) if (ReadingsVal($name,"a".$sensor."_temperature",0) != $temperature); + } + } elsif ($clientmsg->{direction} eq "i2cwrite" && defined($clientmsg->{data})) { + #reply from write - ignore + } + readingsEndUpdate($hash, 1); + } +} + +1; + +#Todo Write update documentation + +=pod +=item device +=item summary reads/converts data from an via I2C connected ADS1x1x A/D converter +=item summary_DE liest/konvertiert Daten eines via angeschlossenen ADS1x1x A/D Wandlers +=begin html + + +

I2C_ADS1x1x

+(en | de) + + +=end html + +=begin html_DE + + +

I2C_ADS1x1x

+(en | de) + + +=end html_DE + +=cut