diff --git a/fhem/CHANGED b/fhem/CHANGED index 2dd8becb0..0d3d57b9f 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: new module 52_I2C_K30.pm added - bugfix: 98_weekprofile: send reference profile to device - feature: 10_pilight_ctrl | 30_pilight_switch: support protocol daycom - feature: 59_Weather: rewritten to use JSON API diff --git a/fhem/FHEM/52_I2C_K30.pm b/fhem/FHEM/52_I2C_K30.pm new file mode 100644 index 000000000..f7fa004e3 --- /dev/null +++ b/fhem/FHEM/52_I2C_K30.pm @@ -0,0 +1,357 @@ +############################################## +# I2C_K30.pm: heavily based on I2C_SHT21.pm +# +# $Id$ + +package main; + +use strict; +use warnings; + +use constant { + # For details, see SenseAir "I2C communication guide for K20/K21/K22/K30 platforms" + K30_I2C_ADDRESS => 0x68, + K30_REQ_READ_RAM => 0x20, + K30_RESP_READ_COMPLETE => 0x21, + K30_ADDR_CO2 => 0x08, + K30_LEN_CO2 => 2 +}; + +################################################## +# Forward declarations +# +sub I2C_K30_Initialize($); +sub I2C_K30_Define($$); +sub I2C_K30_Attr(@); +sub I2C_K30_Poll($); +sub I2C_K30_Set($@); +sub I2C_K30_Undef($$); +sub I2C_K30_DbLog_splitFn($); + +my %sets = ( + 'readValues' => 1, +); + +sub I2C_K30_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = 'I2C_K30_Define'; + $hash->{InitFn} = 'I2C_K30_Init'; + $hash->{AttrFn} = 'I2C_K30_Attr'; + $hash->{SetFn} = 'I2C_K30_Set'; + $hash->{UndefFn} = 'I2C_K30_Undef'; + $hash->{I2CRecFn} = 'I2C_K30_I2CRec'; + $hash->{AttrList} = 'IODev do_not_notify:0,1 showtime:0,1 poll_interval:1,2,5,10,20,30 ' . + $readingFnAttributes; + $hash->{DbLog_splitFn} = "I2C_K30_DbLog_splitFn"; +} + +sub I2C_K30_Define($$) { + my ($hash, $def) = @_; + my @a = split('[ \t][ \t]*', $def); + + $hash->{STATE} = "defined"; + + if ($main::init_done) { + eval { I2C_K30_Init( $hash, [ @a[ 2 .. scalar(@a) - 1 ] ] ); }; + return I2C_K30_Catch($@) if $@; + } + return undef; +} + +sub I2C_K30_Init($$) { + my ( $hash, $args ) = @_; + + my $name = $hash->{NAME}; + + if (defined $args && int(@$args) > 1) + { + Log3 $hash, 1, "Define: Wrong syntax. Can't initialize sensor."; + return; + } + + if (defined (my $address = shift @$args)) { + $hash->{I2C_Address} = $address =~ /^0.*$/ ? oct($address) : $address; + return "$name I2C Address not valid" unless ($address < 128 && $address > 3); + } else { + $hash->{I2C_Address} = K30_I2C_ADDRESS; + } + + + my $msg = ''; + # create default attributes + if (AttrVal($name, 'poll_interval', '?') eq '?') { + $msg = CommandAttr(undef, $name . ' poll_interval 5'); + if ($msg) { + Log3 ($hash, 1, $msg); + return $msg; + } + } + AssignIoPort($hash); + $hash->{STATE} = 'Initialized'; + + return undef; +} + +sub I2C_K30_Catch($) { + my $exception = shift; + if ($exception) { + $exception =~ /^(.*)( at.*FHEM.*)$/; + return $1; + } + return undef; +} + +sub I2C_K30_Attr (@) {# hier noch Werteueberpruefung einfuegen + my ($command, $name, $attr, $val) = @_; + my $hash = $defs{$name}; + my $msg = ''; + if ($command && $command eq "set" && $attr && $attr eq "IODev") { + eval { + if ($main::init_done and (!defined ($hash->{IODev}) or $hash->{IODev}->{NAME} ne $val)) { + main::AssignIoPort($hash,$val); + my @def = split (' ',$hash->{DEF}); + I2C_K30_Init($hash,\@def) if (defined ($hash->{IODev})); + } + }; + return I2C_K30_Catch($@) if $@; + } + if ($attr eq 'poll_interval') { + if ($val > 0) { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday() + 5, 'I2C_K30_Poll', $hash, 0); + } else { + $msg = 'Wrong poll intervall defined. poll_interval must be a number > 0'; + } + } + return ($msg) ? $msg : undef; +} + +sub I2C_K30_Poll($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + # Read values + I2C_K30_Set($hash, ($name, 'readValues')); + + my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 0); + if ($pollInterval > 0) { + InternalTimer(gettimeofday() + ($pollInterval * 60), 'I2C_K30_Poll', $hash, 0); + } +} + +sub I2C_K30_Set($@) { + my ($hash, @a) = @_; + my $name = $a[0]; + my $cmd = $a[1]; + + if(!defined($sets{$cmd})) { + return 'Unknown argument ' . $cmd . ', choose one of ' . join(' ', keys %sets) + } + + if ($cmd eq 'readValues') { + I2C_K30_readCO2($hash); + } +} + +sub I2C_K30_Undef($$) { + my ($hash, $arg) = @_; + + RemoveInternalTimer($hash); + return undef; +} + +sub I2C_K30_I2CRec ($$) { + my ($hash, $clientmsg) = @_; + my $name = $hash->{NAME}; + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + 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/ ; + } + + # Read Complete Response + if ( $clientmsg->{direction} && $clientmsg->{$pname . "_SENDSTAT"} && $clientmsg->{$pname . "_SENDSTAT"} eq "Ok" ) { + if ( $clientmsg->{direction} eq "i2cread" && defined($clientmsg->{received}) ) { + Log3 $hash, 4, "empfangen: $clientmsg->{received}"; + my @raw = split(" ",$clientmsg->{received}); + I2C_K30_ParseCO2 ($hash, $clientmsg->{received}) if ($raw[0] == K30_RESP_READ_COMPLETE) && $clientmsg->{nbyte} == 4; + } + } +} + +sub I2C_K30_ParseCO2 ($$) { + my ($hash, $rawdata) = @_; + my @raw = split(" ",$rawdata); + if ( defined (my $crc = I2C_K30_CheckCrc(@raw)) ) { #CRC Test + Log3 $hash, 3, "CRC error CO2 data: $rawdata, Checksum calculated: $crc"; + $hash->{CRCError}++; + return; + } + my $co2 = $raw[1] << 8 | $raw[2]; + $co2 = sprintf('%i', $co2); + readingsBeginUpdate($hash); + readingsBulkUpdate( + $hash, + 'state', + 'CO2: ' . $co2 + ); + readingsBulkUpdate($hash, 'CO2', $co2); + readingsEndUpdate($hash, 1); +} + +sub I2C_K30_readCO2($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + return "$name: no IO device defined" unless ($hash->{IODev}); + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + + my $i2creq = { i2caddress => $hash->{I2C_Address}, direction => "i2cwrite" }; + # read CO2 from sensor RAM, last byte: "checksum" of all preceding bytes + $i2creq->{data} = join(" ", (K30_REQ_READ_RAM | K30_LEN_CO2, 0, K30_ADDR_CO2, (K30_REQ_READ_RAM + K30_LEN_CO2 + 0 + K30_ADDR_CO2) ) ); + CallFn($pname, "I2CWrtFn", $phash, $i2creq); + RemoveInternalTimer($hash); + InternalTimer(gettimeofday() + 1, 'I2C_K30_readValue', $hash, 0); #nach 1s Wert lesen (min. 20ms lt. Datenblatt) + return; +} + +sub I2C_K30_readValue($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + return "$name: no IO device defined" unless ($hash->{IODev}); + my $phash = $hash->{IODev}; + my $pname = $phash->{NAME}; + + # Reset Internal Timer to Poll Sub + RemoveInternalTimer($hash); + my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 0); + InternalTimer(gettimeofday() + ($pollInterval * 60), 'I2C_K30_Poll', $hash, 0) if ($pollInterval > 0); + # Read the three byte result from device + 1byte CRC + my $i2cread = { i2caddress => $hash->{I2C_Address}, direction => "i2cread" }; + $i2cread->{nbyte} = 4; + CallFn($pname, "I2CWrtFn", $phash, $i2cread); + + return; +} + +sub I2C_K30_CheckCrc(@) { + my @data = @_; + my $crc = 0; + for (my $n = 0; $n < (scalar(@data) - 1); ++$n) { + $crc += $data[$n]; + } + return ($crc = $data[3] ? undef : $crc); +} + +sub I2C_K30_DbLog_splitFn($) { + my ($event) = @_; + Log3 undef, 3, "in DbLog_splitFn empfangen: $event"; + my ($reading, $value, $unit) = ""; + my @parts = split(/ /,$event); + $reading = shift @parts; + $reading =~ tr/://d; + $value = $parts[0]; + $unit = "ppm" if(lc($reading) =~ m/CO2/); + return ($reading, $value, $unit); +} + +1; + +=pod +=begin html + + +
define <name> I2C_K30 [<I2C Address>]
<I2C Address>
is the configured I2C address of the sensor (default: 104, i.e. 0x68) set <name> readValues
define <name> I2C_K30 [<I2C Address>]
<I2C Address>
ist die konfigurierte I2C-Adresse des Sensors (Standard: 104 bzw. 0x68)set <name> readValues