diff --git a/FHEM/51_I2C_BH1750.pm b/FHEM/51_I2C_BH1750.pm new file mode 100644 index 000000000..c1558d54f --- /dev/null +++ b/FHEM/51_I2C_BH1750.pm @@ -0,0 +1,681 @@ +# $Id$ +############################################################################## +# +# 51_I2C_BH1750.pm +# +# This file is part of FHEM. +# +# Fhem 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. +# +# BDKM 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. +# +# You should have received a copy of the GNU General Public License +# along with FHEM. If not, see . +# +# Written by Arno Augustin +############################################################################## + + +package main; + +use strict; +use warnings; + +use Time::HiRes qw(tv_interval); + +# BH1750 chip constants +use constant +{ + # I2C addresses + BH1750_ADDR_DEFAULT => 0x23, + BH1750_ADDR_OTHER => 0x5C, + + # I2C registers + BH1750_RAW_VALUE => 0x00, + BH1750_POWER_DOWN => 0x00, + BH1750_POWER_ON => 0x01, + BH1750_RESET => 0x07, + BH1750_H_MODE => 0x10, + BH1750_H_MODE2 => 0x11, + BH1750_L_MODE => 0x13, + BH1750_H_MODE_ONCE => 0x20, + BH1750_H_MODE2_ONCE => 0x21, + BH1750_L_MODE_ONCE => 0x23, + + BH1750_MT_MIN => 31, + BH1750_MT_DEFAULT => 69, + BH1750_MT_MAX => 254, +}; + +#state +use constant +{ + BH1750_STATE_DEFINED => 'Defined', + BH1750_STATE_I2C_ERROR => 'I2C Error', + BH1750_STATE_SATURATED => 'Saturated', + BH1750_STATE_OK => 'Ok' +}; + +# PollState +use constant +{ + BH1750_POLLSTATE_IDLE => 0, + BH1750_POLLSTATE_START_MEASURE => 1, + BH1750_POLLSTATE_PRE_LUX_WAIT => 2, + BH1750_POLLSTATE_PRE_LUX_DONE => 3, + BH1750_POLLSTATE_LUX_WAIT => 4, + BH1750_POLLSTATE_LUX_DONE => 5 +}; + +# chip parameter selection for different LUX values +# BH1750 has the following limitations: +# MODE_L, MT= 31, LUX 0-121556, res. >8 LUX +# MODE_L, MT= 69, LUX 0- 54612, res. >4 LUX +# MODE2_H, MT= 31, LUX 0- 60778, res. >1 LUX +# MODE2_H, MT= 69, LUX 0- 27306, res. >0.5 LUX +# MODE2_H, MT=254, LUX 0- 7417, res. >0.11 LUX + +my @I2C_BH1750_ranges=( + [ 5500,254, BH1750_H_MODE2_ONCE], + [ 11000,127, BH1750_H_MODE2_ONCE], + [ 22000, 69, BH1750_H_MODE2_ONCE], + [ 50000, 31, BH1750_H_MODE2_ONCE] + # else use 31, BH1750_L_MODE_ONCE +); + +sub I2C_BH1750_Initialize($); +sub I2C_BH1750_Define($$); +sub I2C_BH1750_Attr(@); +sub I2C_BH1750_Poll($); +sub I2C_BH1750_Restart_Measure($); +sub I2C_BH1750_Set($@); +sub I2C_BH1750_Get($); +sub I2C_BH1750_Undef($$); + +my $libcheck_hasHiPi = 1; + +sub I2C_BH1750_Initialize($) +{ + my ($hash) = @_; + + eval "use HiPi::Device::I2C;"; + $libcheck_hasHiPi = 0 if($@); + + $hash->{STATE} = "Init"; + $hash->{DefFn} = "I2C_BH1750_Define"; + $hash->{UndefFn} = "I2C_BH1750_Undef"; + $hash->{InitFn} = "I2C_BH1750_Init"; + $hash->{AttrFn} = "I2C_BH1750_Attr"; + $hash->{SetFn} = "I2C_BH1750_Set"; + $hash->{I2CRecFn} = 'I2C_BH1750_I2CRec'; + $hash->{AttrList} = "poll_interval:1,2,5,10,20,30 IODev percentdelta ". + $readingFnAttributes; + $hash->{AttrList} .= " useHiPiLib:0,1 " if ($libcheck_hasHiPi); + $hash->{VERSION} = '$Id$'; +} + +sub I2C_BH1750_Define($$) +{ + my ($hash, $def) = @_; + my @a = split(/\s+/, $def); + my $name = shift(@a); + my $device=0; + + my $usage=sprintf "usage: \"define I2C_BH1750 [devicename] [0x%x|0x%x]\"\n", + BH1750_ADDR_DEFAULT,BH1750_ADDR_OTHER; + + $hash->{I2C_Address}=BH1750_ADDR_DEFAULT; # default + $hash->{HiPi_used} = 0; + + shift(@a); + if(@a) { + $_=shift(@a); + if(m+^/dev/+) { + $device = $_; + if ($libcheck_hasHiPi) { + $hash->{HiPi_used} = 1; + } else { + return '$name error: HiPi library not installed'; + } + if(@a) { + $_=shift(@a); + /0x(5c|23)/i or return $usage; + $hash->{I2C_Address}=hex($_); + } + } elsif (/0x(5c|23)/i) { + $hash->{I2C_Address}=hex($_); + } else { + return $usage; + } + @a and return $usage; + } + + $hash->{BASEINTERVAL} = 0; + $hash->{DELTA} = 0; + $hash->{PollState} = BH1750_POLLSTATE_IDLE; + + readingsSingleUpdate($hash, 'state', BH1750_STATE_DEFINED, 1); + if ($main::init_done || $hash->{HiPi_used}) { + eval { + I2C_BH1750_Init($hash, [ $device ]); + }; + Log3 ($hash, 1, $hash->{NAME} . ': ' . I2C_BH1750_Catch($@)) if $@;; + } + + return undef; +} + +sub I2C_BH1750_Init($$) +{ + my ($hash, $dev) = @_; + my $name = $hash->{NAME}; + + if ($hash->{HiPi_used}) { + # check for existing i2c device + my $i2cModulesLoaded = 0; + $i2cModulesLoaded = 1 if -e $dev; + if ($i2cModulesLoaded) { + if (-r $dev && -w $dev) { + $hash->{devBH1750} = HiPi::Device::I2C->new( + devicename => $dev, + address => $hash->{I2C_Address}, + busmode => 'i2c', + ); + Log3 $name, 3, "I2C_BH1750_Define device created"; + } else { + my @groups = split '\s', $(; + return "$name :Error! $dev isn't readable/writable by user " . + getpwuid( $< ) . " or group(s) " . + getgrgid($_) . " " foreach(@groups); + } + } else { + return $name . ': Error! I2C device not found: ' . $dev . + '. Please check that these kernelmodules are loaded: i2c_bcm2708, i2c_dev'; + } + } else { + AssignIoPort($hash); + } + + + # start new measurement cycle + RemoveInternalTimer($hash); + InternalTimer(gettimeofday() + 10, 'I2C_BH1750_Poll', $hash, 0); + + return undef; +} + +sub I2C_BH1750_Catch($) +{ + my $exception = shift; + if ($exception) { + $exception =~ /^(.*)( at.*FHEM.*)$/; + return $1; + } + return undef; +} + + + +sub I2C_BH1750_Attr (@) +{ + my ($cmd,$name,$attr,$val) = @_; + my $hash = $defs{$name}; + my $error = "$name: ERROR attribute $attr "; + my $del = $cmd =~ /del/; + local $_; + + Log3 $name, 3, "I2C_BH1750_Attr $cmd,$name,$attr,$val"; + + if ($attr eq "percentdelta") { + if($del) { + $val = 0; # default + } else { + $val =~ /^([0-9]+|[0-9]+\.?[0.9]+)$/ or return $error."needs numeric value"; + } + $hash->{DELTA} = $val/100; + } elsif ($attr eq "poll_interval") { + RemoveInternalTimer($hash); + if($del) { + $hash->{BASEINTERVAL} = 0; + $hash->{PollState} = BH1750_POLLSTATE_IDLE; + } else { + if($val !~ /^\d+$/ or $val < 1) { + return $error."needs interger value"; + } else { + $hash->{BASEINTERVAL} = 60*$val; + I2C_BH1750_Restart_Measure($hash); + } + } + } + + return undef; +} + +sub I2C_BH1750_Restart_Measure($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + RemoveInternalTimer($hash); + $hash->{PollState} = BH1750_POLLSTATE_IDLE; + my $delay=$hash->{BASEINTERVAL}; + I2C_BH1750_i2cwrite($hash, BH1750_POWER_DOWN); + $delay and InternalTimer(gettimeofday() + 10, 'I2C_BH1750_Poll', $hash, 0); +} + + +sub I2C_BH1750_Poll($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, "I2C_BH1750_Poll ".gettimeofday()." PollState=$hash->{PollState}"; + RemoveInternalTimer($hash); + my $delay=$hash->{BASEINTERVAL}; + $hash->{PollState} == BH1750_POLLSTATE_IDLE and $hash->{PollState}++; + my $state = $hash->{PollState}; + if ($state == BH1750_POLLSTATE_START_MEASURE) { + I2C_BH1750_i2cwrite($hash, BH1750_POWER_ON); + # check in lowest resolution first + $delay = I2C_BH1750_start_measure($hash,BH1750_MT_MIN,BH1750_L_MODE_ONCE); + $hash->{PollState} = BH1750_POLLSTATE_PRE_LUX_WAIT; + } elsif($state == BH1750_POLLSTATE_PRE_LUX_WAIT) { + I2C_BH1750_request_measure($hash); + } elsif($state == BH1750_POLLSTATE_PRE_LUX_DONE) { + my $lux=$hash->{LUX}; + my $i; + for($i=0; $i<@I2C_BH1750_ranges; $i++) { + $lux <= $I2C_BH1750_ranges[$i][0] and last; + } + if($i == @I2C_BH1750_ranges) { + $hash->{PollState} = BH1750_POLLSTATE_LUX_DONE; + return I2C_BH1750_Poll($hash); + } else { + # do finer reading + my (undef,$mt,$mode)=@{$I2C_BH1750_ranges[$i]}; + Log3 $name, 4, "I2C_BH1750_Poll using mt=$mt, mode=$mode"; + $delay=I2C_BH1750_start_measure($hash,$mt,$mode); + $hash->{PollState} = BH1750_POLLSTATE_LUX_WAIT; + } + } elsif($state == BH1750_POLLSTATE_LUX_WAIT) { + I2C_BH1750_request_measure($hash); + } elsif($state == BH1750_POLLSTATE_LUX_DONE) { + I2C_BH1750_update_lux($hash); # no finer resolution possible + $hash->{PollState} = BH1750_POLLSTATE_IDLE; + I2C_BH1750_i2cwrite($hash, BH1750_POWER_DOWN); + } else { + Log3 $name, 1, "I2C_BH1750_Poll wrong state state=$state"; + $hash->{PollState} = BH1750_POLLSTATE_IDLE; + } + + $delay and InternalTimer(gettimeofday() + $delay, 'I2C_BH1750_Poll', $hash, 0); + + return undef; +} + +sub I2C_BH1750_Set($@) +{ + my ( $hash, @args ) = @_; + my $name = $hash->{NAME}; + my $cmd = $args[1]; + + if($cmd eq "update") { + RemoveInternalTimer($hash); + $hash->{PollState} = BH1750_POLLSTATE_START_MEASURE; + I2C_BH1750_Poll($hash); + } elsif($cmd eq "deleteminmax") { + delete($hash->{READINGS}{minimum}); + delete($hash->{READINGS}{maximum}); + } else { + return "Unknown argument ".$cmd.", choose one of update deleteminmax"; + } + + return undef; +} + +sub I2C_BH1750_Undef($$) +{ + my ($hash, $arg) = @_; + + RemoveInternalTimer($hash); + + return undef; +} + + +sub I2C_BH1750_i2cread($$$) +{ + my ($hash, $reg, $nbyte) = @_; + my $success = 1; + + Log3 $hash->{NAME}, 5, "I2C_BH1750_i2cread $reg,$nbyte"; + + local $SIG{__WARN__} = sub { + my $message = shift; + # turn warnings from RPII2C_HWACCESS_ioctl into exception + if ($message =~ /Exiting subroutine via last at.*00_RPII2C.pm/) { + die; + } else { + warn($message); + } + }; + + if ($hash->{HiPi_used}) { + eval { + my @values = $hash->{devBH1750}->bus_read($reg, $nbyte); + I2C_BH1750_I2CRec($hash, { + direction => "i2cread", + i2caddress => $hash->{I2C_Address}, + reg => $reg, + nbyte => $nbyte, + received => join (' ',@values), + }); + }; + Log3 ($hash, 1, $hash->{NAME} . ': ' . I2C_BH1750_Catch($@)) if $@; + } elsif (defined (my $iodev = $hash->{IODev})) { + eval { + CallFn($iodev->{NAME}, "I2CWrtFn", $iodev, { + direction => "i2cread", + i2caddress => $hash->{I2C_Address}, + reg => $reg, + nbyte => $nbyte + }); + }; + my $sendStat = $hash->{$iodev->{NAME}.'_SENDSTAT'}; + if (defined($sendStat) && $sendStat eq 'error') { + readingsSingleUpdate($hash, 'state', BH1750_STATE_I2C_ERROR, 1); + Log3 ($hash, 5, $hash->{NAME} . ": i2cread on $iodev->{NAME} failed"); + $success = 0; + } + } else { + Log3 ($hash, 1, $hash->{NAME} . ': ' . "no IODev assigned to '$hash->{NAME}'"); + $success = 0; + } + + return $success; +} + +sub I2C_BH1750_i2cwrite +{ + my ($hash, $reg, @data) = @_; + my $success = 1; + + Log3 $hash->{NAME}, 5, "I2C_BH1750_i2write $reg,@data"; + if ($hash->{HiPi_used}) { + eval { + $hash->{devBH1750}->bus_write($reg, join (' ',@data)); + I2C_BH1750_I2CRec($hash, { + direction => "i2cwrite", + i2caddress => $hash->{I2C_Address}, + reg => $reg, + data => join (' ',@data), + }); + }; + Log3 ($hash, 1, $hash->{NAME} . ': ' . I2C_BH1750_Catch($@)) if $@; + } elsif (defined (my $iodev = $hash->{IODev})) { + eval { + CallFn($iodev->{NAME}, "I2CWrtFn", $iodev, { + direction => "i2cwrite", + i2caddress => $hash->{I2C_Address}, + reg => $reg, + data => join (' ',@data), + }); + }; + my $sendStat = $hash->{$iodev->{NAME}.'_SENDSTAT'}; + if (defined($sendStat) && $sendStat eq 'error') { + readingsSingleUpdate($hash, 'state', BH1750_STATE_I2C_ERROR, 1); + Log3 ($hash, 5, $hash->{NAME} . ": i2cwrite on $iodev->{NAME} failed"); + $success = 0; + } + } else { + Log3 ($hash, 1, $hash->{NAME} . ': ' . "no IODev assigned to '$hash->{NAME}'"); + $success = 0; + } + + return $success; +} + +sub I2C_BH1750_I2CRec ($$) +{ + my ($hash, $clientmsg) = @_; + my $name = $hash->{NAME}; + + Log3 $hash->{NAME}, 5, "I2C_BH1750_i2Rec"; + my $pname = undef; + unless ($hash->{HiPi_used}) { + my $phash = $hash->{IODev}; + $pname = $phash->{NAME}; + while (my ( $k, $v ) = each %$clientmsg) { + $hash->{$k} = $v if $k =~ /^$pname/; + Log3 $hash->{NAME}, 5, "I2C_BH1750_i2Rec $k $v"; + } + if($clientmsg->{$pname . "_SENDSTAT"} ne "Ok") { + Log3 $hash->{NAME}, 3, "I2C_BH1750_i2Rec bad sendstat: ".$clientmsg->{$pname."_SENDSTAT"}; + if($clientmsg->{direction} eq "i2cread" or $clientmsg->{reg}) { + # avoid recoursion on power down + I2C_BH1750_Restart_Measure($hash); + } + return undef; + } + + } + + if ( $clientmsg->{direction} eq "i2cread" && defined($clientmsg->{received})) { + my $register = $clientmsg->{reg}; + Log3 $hash, 4, "$name RX register $register, $clientmsg->{nbyte} byte: $clientmsg->{received}"; + my $byte = undef; + my $word = undef; + my @raw = split(" ", $clientmsg->{received}); + if ($clientmsg->{nbyte} == 1) { + $byte = $raw[0]; + } elsif ($clientmsg->{nbyte} == 2) { + $word = $raw[0] << 8 | $raw[1]; + } + if ($register == BH1750_RAW_VALUE) { + if($word == 0xFFFF) { + readingsSingleUpdate($hash, 'state', BH1750_STATE_SATURATED, 1); + Log3 $hash, 3, "$name sensor saturated "; + I2C_BH1750_Restart_Measure($hash); + } else { + readingsSingleUpdate($hash, 'state', BH1750_STATE_OK, 1); + my $lux = $word/1.2*(69/$hash->{MT_VAL})/$hash->{MODE}; + $hash->{LUX}=$lux; + Log3 $hash->{NAME}, 4, "I2C_BH1750_I2CRec: lux=$lux"; + if($hash->{PollState} == BH1750_POLLSTATE_PRE_LUX_WAIT || + $hash->{PollState} == BH1750_POLLSTATE_LUX_WAIT) { + $hash->{PollState}++; + I2C_BH1750_Poll($hash); + } + } + } + } + return undef; +} + +sub I2C_BH1750_update_lux +{ + my($hash)=@_; + my $name=$hash->{NAME}; + my $lux=$hash->{LUX}; + my $delta=$hash->{DELTA}; + + if($delta) { # update only if delta large enough + my $lastlux=ReadingsNum($name,"luminosity",1000000); + $lux == $lastlux and return; # no delta, no update + + # check if we have too less delta and return + if($lastlux > $lux) { + ($lastlux-$lastlux*$delta < $lux) and return; + } else { + ($lastlux+$lastlux*$delta > $lux) and return; + } + } + # round value + if($lux < 100) { + $lux = int($lux*10+0.5)/10; + } elsif($lux < 1000) { + $lux=int($lux+0.5); + } elsif($lux < 10000) { + $lux=int($lux/10+0.5); $lux *= 10; + } elsif($lux < 100000) { + $lux=int($lux/100+0.5); $lux *= 100; + } else { + $lux=int($lux/1000+0.5); $lux *= 1000; + } + Log3 $hash->{NAME}, 4, "I2C_BH1750_update_lux: luminosity=$lux"; + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "luminosity", $lux); + $lux < ReadingsNum($name,"minimum", 1000000) and readingsBulkUpdate($hash, "minimum", $lux); + $lux > ReadingsNum($name,"maximum",-1000000) and readingsBulkUpdate($hash, "maximum", $lux); + readingsEndUpdate($hash, 1); +} + +sub I2C_BH1750_request_measure +{ + my ($hash) = @_; + I2C_BH1750_i2cread($hash,BH1750_RAW_VALUE,2); +} + + +sub I2C_BH1750_start_measure +{ + my ($hash,$mt,$mode)=@_; + $hash->{MT_VAL} = $mt; + $hash->{MODE} = ($mode == BH1750_H_MODE2 || $mode == BH1750_H_MODE2_ONCE) ? 2 : 1; + my $hi=($mt>>5) | 0x40; + my $lo=($mt&0x1F) | 0x60; + I2C_BH1750_i2cwrite($hash,BH1750_RESET); + I2C_BH1750_i2cwrite($hash,$hi); # set MT value + I2C_BH1750_i2cwrite($hash,$lo); + I2C_BH1750_i2cwrite($hash,$mode); + my $mindelay = ($mode == BH1750_L_MODE || $mode == BH1750_L_MODE_ONCE) ? 24 : 180; + $mindelay = $mindelay * ($mt/BH1750_MT_DEFAULT); + Log3 $hash->{NAME}, 5, "I2C_BH1750_start_measure: duration ".int($mindelay)."ms"; + $mindelay /=1000; # seconds + + return $mindelay; +} + +sub I2C_BH1750_sleep +{ + select(undef, undef, undef, $_[0]); +} + +1; + +=pod +=begin html + + +

I2C_BH1750

+ +=end html + +=cut