diff --git a/fhem/FHEM/38_CO20.pm b/fhem/FHEM/38_CO20.pm index 9fa644589..d9d21136b 100755 --- a/fhem/FHEM/38_CO20.pm +++ b/fhem/FHEM/38_CO20.pm @@ -19,11 +19,14 @@ CO20_Initialize($) $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "CO20_Notify"; $hash->{UndefFn} = "CO20_Undefine"; - #$hash->{SetFn} = "CO20_Set"; + $hash->{SetFn} = "CO20_Set"; $hash->{GetFn} = "CO20_Get"; $hash->{AttrFn} = "CO20_Attr"; $hash->{AttrList} = "disable:1 ". + "advanced:1 ". "interval ". + "retries ". + "timeout ". $readingFnAttributes; } @@ -47,6 +50,10 @@ CO20_Define($$) $hash->{NAME} = $name; + $hash->{fail} = 0; + $hash->{seq2} = 0x67; + $hash->{seq4} = 0x0001; + if( $init_done ) { CO20_Disconnect($hash); CO20_Connect($hash); @@ -71,6 +78,61 @@ CO20_Notify($$) my $VENDOR = 0x03eb; my $PRODUCT = 0x2013; + +sub +CO20_SetStickData($$) +{ + + my ($hash, $data) = @_; + my $name = $hash->{NAME}; + + my $strlen = length($data); + my $ind = 0; +Log3 $name, 5, "datalen $strlen"; + + if($strlen == 240) { + $ind = index($data, "warn1")+22; + $hash->{KNOB_CO2_VOC_level_warn1} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "warn2")+22; + $hash->{KNOB_CO2_VOC_level_warn2} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "Reg_Set")+20; + $hash->{KNOB_Reg_Set} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "Reg_P")+19; + $hash->{KNOB_Reg_P} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "Reg_I")+19; + $hash->{KNOB_Reg_I} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "Reg_D")+19; + $hash->{KNOB_Reg_D} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "LogInterval")+27; + $hash->{KNOB_LogInterval} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + $ind = index($data, "ui16StartupBits")+30; + $hash->{KNOB_ui16StartupBits} = ord(substr($data,$ind+1,1))*256 + ord(substr($data,$ind,1)); + } elsif($strlen == 32) { + $ind = index($data, ";"); + $hash->{FLAG_WARMUP} = ord(substr($data,$ind+3,1))*256 + ord(substr($data,$ind+2,1)); + $hash->{FLAG_BURN_IN} = ord(substr($data,$ind+7,1))*256 + ord(substr($data,$ind+6,1)); + $hash->{FLAG_RESET_BASELINE} = ord(substr($data,$ind+11,1))*256 + ord(substr($data,$ind+10,1)); + $hash->{FLAG_CALIBRATE_HEATER} = ord(substr($data,$ind+15,1))*256 + ord(substr($data,$ind+14,1)); + $hash->{FLAG_LOGGING} = ord(substr($data,$ind+19,1))*256 + ord(substr($data,$ind+18,1)); + } elsif($strlen == 1) { + delete( $hash->{KNOB_CO2_VOC_level_warn1} ); + delete( $hash->{KNOB_CO2_VOC_level_warn2} ); + delete( $hash->{KNOB_Reg_Set} ); + delete( $hash->{KNOB_Reg_P} ); + delete( $hash->{KNOB_Reg_I} ); + delete( $hash->{KNOB_Reg_D} ); + delete( $hash->{KNOB_LogInterval} ); + delete( $hash->{KNOB_ui16StartupBits} ); + delete( $hash->{FLAG_WARMUP} ); + delete( $hash->{FLAG_BURN_IN} ); + delete( $hash->{FLAG_RESET_BASELINE} ); + delete( $hash->{FLAG_CALIBRATE_HEATER} ); + delete( $hash->{FLAG_LOGGING} ); + } + + return undef; +} + sub CO20_Connect($) { @@ -121,12 +183,13 @@ CO20_Connect($) } elsif( $ret != 0 ) { Log3 $name, 3, "$name: failed to claim CO20 device"; CO20_Disconnect($hash); + return; } $hash->{STATE} = "opened"; Log3 $name, 3, "$name: CO20 device opened"; - my $interval = AttrVal($name, "interval", 0); + my $interval = AttrVal($name, "interval", 300); $interval = 60*5 if( !$interval ); $hash->{INTERVAL} = $interval; @@ -163,6 +226,10 @@ CO20_Disconnect($) delete( $hash->{manufacturer} ); delete( $hash->{product} ); + delete( $hash->{BLOCKED} ); + delete $hash->{FIRMWARE}; + CO20_SetStickData($hash,"X"); + $hash->{STATE} = "disconnected"; Log3 $name, 3, "$name: disconnected"; } @@ -173,17 +240,16 @@ CO20_Undefine($$) my ($hash, $arg) = @_; CO20_Disconnect($hash); + $hash->{fail} = 0; return undef; } sub -CO20_Set($$@) +CO20_identify($) { - my ($hash, $name, $cmd) = @_; - - my $list = ""; - return "Unknown argument $cmd, choose one of $list"; + my ($hash) = @_; + CO20_dataread($hash,"stickdata"); } sub @@ -197,41 +263,363 @@ CO20_poll($) InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 0); } - if( $hash->{manufacturer} && $hash->{product} ) { - my $buf = "\x40\x68\x2a\x54\x52\x0a\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40"; - my $ret = $hash->{DEV}->interrupt_write(0x00000002, $buf, 0x0000010, 1000); + if($hash->{BLOCKED}) { + return undef; + } + + if( $hash->{manufacturer} && $hash->{product} ) { + + + + my $buf = "@".sprintf("%c",$hash->{seq2})."TRF?\n@@@@@@@@@"; + + Log3 $name, 5, "$name: sent $buf / ".ord(substr($buf,0,1)); + + my $ret = $hash->{DEV}->interrupt_write(0x00000002, $buf, 0x0000010, $hash->{timeout}); + if( $ret != 16 ) { + my $ret2 = $hash->{DEV}->interrupt_write(0x00000002, "@@@@@@@@@@@@@@@@", 0x0000010, $hash->{timeout}); + $hash->{fail} = $hash->{fail}+1; + Log3 $name, 4, "$name: write error $ret/$ret2 ($hash->{fail})"; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+30, "CO20_poll", $hash, 1); + if($hash->{fail} >= $hash->{retries}) { + $hash->{fail} = 0; + CO20_Disconnect($hash); + $hash->{RECONNECT} = 1; + CO20_Connect($hash); + } + return undef; + } + if ($hash->{seq2} < 0xFF){ $hash->{seq2}++} else {$hash->{seq2} = 0x67}; + +my $data=""; +for( $a = 1; $a <= 3; $a = $a + 1 ){ + $ret=$hash->{DEV}->interrupt_read(0x00000081, $buf, 0x0000010, $hash->{timeout}); + if( $ret != 16 and $ret != 0 ) { + Log3 $name, 4, "$name: read error $ret"; + } + $data.=$buf; +} +Log3 $name, 4, "$name got $data / ".length($data)." / ".ord(substr($data,0,1)); + + if( $ret != 16 and $ret != 0 and length($data) < 16 ) { + $hash->{fail} = $hash->{fail}+1; + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+30, "CO20_poll", $hash, 1); + Log3 $name, 4, "$name: readloop error $ret ($hash->{fail})"; + if($hash->{fail} >= $hash->{retries}) { + $hash->{fail} = 0; + CO20_Disconnect($hash); + $hash->{RECONNECT} = 1; + CO20_Connect($hash); + } + return undef; + } + + + if( length($data) >= 16 ) { + + $data = "@".$data if(ord(substr($data,0,1)) > 64); + + $hash->{fail} = 0; + my $voc = ord(substr($data,3,1))*256 + ord(substr($data,2,1)); + my $dbg = ord(substr($data,5,1))*256 + ord(substr($data,4,1)); + my $pwm = ord(substr($data,7,1))*256 + ord(substr($data,6,1)); + my $rh = ord(substr($data,9,1))*256 + ord(substr($data,8,1)); + my $rs = ord(substr($data,14,1))*65536 + ord(substr($data,13,1))*256 + ord(substr($data,12,1)); + if (ord(substr($data,3,1)) < 128) { + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "voc", $voc, 1 ); + readingsBulkUpdate( $hash, "debug", $dbg, 1 ); + readingsBulkUpdate( $hash, "pwm", $pwm, 1 ); + readingsBulkUpdate( $hash, "r_h", $rh/100, 1 ); + readingsBulkUpdate( $hash, "r_s", $rs, 1 ); + readingsEndUpdate($hash,1); + } + +#my $bufdec = ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1))." ".ord(substr($buf,16,1)); +# Log3 $name, 5, "$name: read 1 success\n$bufdec"; + - $ret = $hash->{DEV}->interrupt_read(0x00000081, $buf, 0x0000010, 1000); - if( $ret == 16 ) { - my $voc = ord(substr($buf,3,1))*256 + ord(substr($buf,2,1)); - readingsSingleUpdate($hash, "voc", $voc, 1 ); - $hash->{DEV}->interrupt_read(0x00000081, $buf, 0x0000010, 1000); } else { - Log3 $name, 3, "$name: read failed"; + $hash->{fail} = $hash->{fail}+1; + Log3 $name, 2, "$name: read failed $ret ($hash->{fail})"; + if($hash->{fail} >= $hash->{retries}) { + $hash->{fail} = 0; CO20_Disconnect($hash); + $hash->{RECONNECT} = 1; CO20_Connect($hash); } + } $hash->{LAST_POLL} = FmtDateTime( gettimeofday() ); } else { + Log3 $name, 2, "$name: no device"; + $hash->{fail} = 0; CO20_Disconnect($hash); + $hash->{RECONNECT} = 1; CO20_Connect($hash); } } +sub +CO20_dataread($$) +{ + my ($hash, $readingstype) = @_; + my $name = $hash->{NAME}; + + + my $reqstr = ""; + my $retcount = 16; + if($readingstype eq "knobdata") { + $reqstr = "KNOBPRE?"; + $retcount = 16; + } elsif ($readingstype eq "flagdata") { + $reqstr = "FLAGGET?"; + $retcount = 3; + } elsif ($readingstype eq "stickdata") { + $reqstr = "*IDN?"; + $retcount = 8; + } else { + return undef; + } + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 1); + + if( $hash->{manufacturer} && $hash->{product} ) { + + + + my $seq = sprintf("%04X",$hash->{seq4}); + my $seqstr = sprintf("%c",hex substr($seq,2,2)).sprintf("%c",hex substr($seq,0,2)); + $hash->{seq4} = ($hash->{seq4} +1) & 0xFFFF; + + my $buf = substr("@".$seq.$reqstr."\n@@@@@@@@@@@@@@@@",0,16); + my $ret = $hash->{DEV}->interrupt_write(0x00000002, $buf, 0x0000010, $hash->{timeout}); +Log3 $name, 4, "getdata write $ret" if($ret != 16); + + +my $data = ""; +my $intdata = ""; +if($ret == 16) { +for( $a = 1; $a <= $retcount; $a = $a + 1 ){ + $hash->{DEV}->interrupt_read(0x00000081, $buf, 0x0000010, $hash->{timeout}); + $data.=$buf; +Log3 $name, 4, "getdata read $ret" if($ret != 16); + $intdata = ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1)); +Log3 $name, 5, "$intdata\n$buf"; +} +Log3 $name, 5, length($data); + +} + + + if($readingstype eq "knobdata") { + CO20_SetStickData($hash,$data); + } elsif ($readingstype eq "flagdata") { + CO20_SetStickData($hash,$data); + } elsif ($readingstype eq "stickdata") { + if ($data =~ /\bStick\b(.*?)\bMCU\b/) { + $hash->{FIRMWARE} = $1; + } + if ($data =~ /\bS\/N:\b(.*?)\b;bI\b/) { + $hash->{SERIALNUMBER} = $1; + } + } + + + } + +} + + +sub +CO20_flashread($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + +# 40 30 30 31 31 52 45 43 4F 52 44 53 3F 0A 40 40 @0011RECORDS?.@@ +# +# 40 30 30 31 32 4C 42 53 49 5A 45 3F 0A 40 40 40 @0012LBSIZE?.@@@ +# +# 40 30 30 31 33 46 4C 53 54 4F 50 0A 40 40 40 40 @0013FLSTOP.@@@@ +# +# 40 30 30 31 34 4C 42 53 49 5A 45 3F 0A 40 40 40 @0014LBSIZE?.@@@ +# +# 40 30 30 31 35 2A 49 44 4E 3F 0A 40 40 40 40 40 @0015*IDN?.@@@@@ +# +# 40 30 30 31 36 4C 42 41 56 47 3B 31 30 30 0A 40 @0016LBAVG;100.@ +# +# 40 6A 4C 42 52 0A 40 40 40 40 40 40 40 40 40 40 @jLBR.@@@@@@@@@@ n times ? +# +# 40 30 30 31 37 46 4C 53 54 41 52 54 0A 40 40 40 @0017FLSTART.@@@ n times ? +# +# 2 reads each + + + + + + +} + +sub +CO20_dataset($$$) +{ + my ($hash, $cmd, $val) = @_; + my $name = $hash->{NAME}; + + my $reqstr = ""; + if($cmd eq "flag_WARMUP") { + $reqstr = "FLAGSET;WARMUP="; # 0000 + } elsif($cmd eq "flag_BURN-IN") { + $reqstr = "FLAGSET;BURN-IN="; # 0000 + } elsif($cmd eq "flag_RESET_BASELINE") { + $reqstr = "FLAGSET;RESET BASELINE="; # 0000 + } elsif($cmd eq "flag_CALIBRATE_HEATER") { + $reqstr = "FLAGSET;CALIBRATE HEATER="; # 0000 + } elsif($cmd eq "flag_LOGGING") { + $reqstr = "FLAGSET;LOGGING="; # 0000 + } elsif($cmd eq "knob_CO2/VOC_level_warn1") { + $reqstr = "KNOBSET;CO2/VOC level_warn1="; + } elsif($cmd eq "knob_CO2/VOC_level_warn2") { + $reqstr = "KNOBSET;CO2/VOC level_warn2="; + } elsif($cmd eq "knob_Reg_Set") { + $reqstr = "KNOBSET;Reg_Set="; # 9100 + } elsif($cmd eq "knob_Reg_P") { + $reqstr = "KNOBSET;Reg_P="; # 0300 + } elsif($cmd eq "knob_Reg_I") { + $reqstr = "KNOBSET;Reg_I="; # 0A00 + } elsif($cmd eq "knob_Reg_D") { + $reqstr = "KNOBSET;Reg_D="; # 0000 + } elsif($cmd eq "knob_LogInterval") { + $reqstr = "KNOBSET;LogInterval="; # 0000 + } elsif($cmd eq "knob_ui16StartupBits") { + $reqstr = "KNOBSET;ui16StartupBits="; # 0000 + } elsif($cmd eq "recalibrate_heater") { + $reqstr = "FLAGSET;CALIBRATE HEATER="; # 0180 + } elsif($cmd eq "reset_baseline") { + $reqstr = "FLAGSET;RESET BASELINE="; # 0180 + } elsif($cmd eq "reset_device") { + $reqstr = "*RST"; + } + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "CO20_poll", $hash, 1); + + if( $hash->{manufacturer} && $hash->{product} ) { + + my $seq = sprintf("%04X",$hash->{seq4}); + $hash->{seq4} = ($hash->{seq4} +1) & 0xFFFF; + + my $buf = "@".$seq.$reqstr; + if($cmd ne "reset_device") { + $buf .= "\x02"; + if($cmd eq "recalibrate_heater" or $cmd eq "reset_baseline") { + $buf .= "\x01\x80"; + } else { + my $h = sprintf("%04X",$val & 0xFFFF); + $buf .= sprintf("%c",hex substr($h,2,2)).sprintf("%c",hex substr($h,0,2)); +Log3 $name, 5, "$val $h \n"; + } + } + if (index($reqstr, "KNOBSET") != -1) { + $buf .= ";"; + } + $buf .= "\n"; + + my $buflen = length($buf); + $buf .= "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"; + + + my $ret = $hash->{DEV}->interrupt_write(0x00000002, substr($buf,0,16), 0x0000010, $hash->{timeout}); + Log3 $name, 4, "setdata write $ret" if($ret != 16); + + + if($ret == 16 and ($buflen > 16 or $cmd eq "reset_device")) { + $ret = $hash->{DEV}->interrupt_write(0x00000002, substr($buf,16,16), 0x0000010, $hash->{timeout}); + Log3 $name, 4, "setdata write $ret" if($ret != 16); + } + + if($ret == 16 and $buflen > 32) { + $ret = $hash->{DEV}->interrupt_write(0x00000002, substr($buf,32,16), 0x0000010, $hash->{timeout}); + Log3 $name, 4, "setdata write $ret" if($ret != 16); + } + + + if($ret == 16) { + $hash->{DEV}->interrupt_read(0x00000081, $buf, 0x0000010, $hash->{timeout}); + Log3 $name, 5, "getdata read $ret"; + my $intdata .= ord(substr($buf,0,1))." ".ord(substr($buf,1,1))." ".ord(substr($buf,2,1))." ".ord(substr($buf,3,1))." ".ord(substr($buf,4,1))." ".ord(substr($buf,5,1))." ".ord(substr($buf,6,1))." ".ord(substr($buf,7,1))." ".ord(substr($buf,8,1))." ".ord(substr($buf,9,1))." ".ord(substr($buf,10,1))." ".ord(substr($buf,11,1))." ".ord(substr($buf,12,1))." ".ord(substr($buf,13,1))." ".ord(substr($buf,14,1))." ".ord(substr($buf,15,1)); + Log3 $name, 4, "$buf"; + } else { + Log3 $name, 4, "set data failed: $buf"; + return undef; + } + + + + + + } + + + + + return undef; + + +} + sub CO20_Get($$@) { my ($hash, $name, $cmd) = @_; my $list = "update:noArg"; + $list = "update:noArg air_data:noArg knob_data:noArg flag_data:noArg stick_data:noArg" if( AttrVal($name, "advanced", 0 ) == 1 ); - if( $cmd eq "update" ) { + if( $cmd eq "air_data" or $cmd eq "update" ) { $hash->{LOCAL} = 1; CO20_poll($hash); delete $hash->{LOCAL}; return undef; + } elsif( $cmd eq "knob_data" ) { + $hash->{BLOCKED} = 1; + CO20_dataread($hash,"knobdata"); + delete $hash->{BLOCKED}; + return undef; + } elsif( $cmd eq "flag_data" ) { + $hash->{BLOCKED} = 1; + CO20_dataread($hash,"flagdata"); + delete $hash->{BLOCKED}; + return undef; + } elsif( $cmd eq "stick_data" ) { + $hash->{BLOCKED} = 1; + CO20_dataread($hash,"stickdata"); + delete $hash->{BLOCKED}; + return undef; + } + + return "Unknown argument $cmd, choose one of $list"; +} + +sub +CO20_Set($$$$) +{ + my ($hash, $name, $cmd, $val) = @_; + + my $list = ""; + $list = "flag_WARMUP flag_BURN-IN flag_RESET_BASELINE flag_CALIBRATE_HEATER flag_LOGGING knob_CO2/VOC_level_warn1 knob_CO2/VOC_level_warn2 knob_Reg_Set knob_Reg_P knob_Reg_I knob_Reg_D knob_LogInterval knob_ui16StartupBits recalibrate_heater:noArg reset_baseline:noArg reset_device:noArg" if( AttrVal($name, "advanced", 0 ) == 1 ); + if (index($list, $cmd) != -1) { + $hash->{BLOCKED} = 1; + CO20_dataset($hash,$cmd,$val); + delete $hash->{BLOCKED}; + return undef; } return "Unknown argument $cmd, choose one of $list"; @@ -243,8 +631,10 @@ CO20_Attr($$$) my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; - $attrVal = int($attrVal) if($attrName eq "interval"); - $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0); + $attrVal = int($attrVal) if($attrName eq "interval" || $attrName eq "retries" || $attrName eq "timeout"); + $attrVal = 30 if($attrName eq "interval" && $attrVal < 30 && $attrVal != 0); + $attrVal = 3 if($attrName eq "retries" && ($attrVal < 0 || $attrVal > 60)); + $attrVal = 1000 if($attrName eq "timeout" && ($attrVal < 500 || $attrVal > 10000)); if( $attrName eq "disable" ) { my $hash = $defs{$name}; @@ -259,6 +649,12 @@ CO20_Attr($$$) my $hash = $defs{$name}; $hash->{INTERVAL} = $attrVal; CO20_poll($hash) if( $init_done ); + } elsif( $attrName eq "retries" ) { + my $hash = $defs{$name}; + $hash->{retries} = $attrVal; + } elsif( $attrName eq "timeout" ) { + my $hash = $defs{$name}; + $hash->{timeout} = $attrVal; } if( $cmd eq "set" ) { @@ -292,6 +688,9 @@ CO20_Attr($$$)