From cccead20e9df5165106e404ec745f38c6d5796c3 Mon Sep 17 00:00:00 2001 From: dominikkarall Date: Sun, 29 Sep 2019 07:10:41 +0000 Subject: [PATCH] 10_GFPROBT: support timers and more git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@20271 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- FHEM/10_GFPROBT.pm | 919 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 690 insertions(+), 229 deletions(-) diff --git a/FHEM/10_GFPROBT.pm b/FHEM/10_GFPROBT.pm index dcc0c9c15..883e23db6 100755 --- a/FHEM/10_GFPROBT.pm +++ b/FHEM/10_GFPROBT.pm @@ -18,10 +18,13 @@ use SetExtensions; use Expect; use JSON; use Blocking; +use Time::Piece; sub GFPROBT_Initialize($) { my ($hash) = @_; - + + $hash->{parseParams} = 1; + $hash->{DefFn} = 'GFPROBT_Define'; $hash->{UndefFn} = 'GFPROBT_Undef'; $hash->{GetFn} = 'GFPROBT_Get'; @@ -33,31 +36,32 @@ sub GFPROBT_Initialize($) { return undef; } -sub GFPROBT_Define($$) { +sub GFPROBT_Define($$$) { #save BTMAC address - my ($hash, $def) = @_; - my @a = split("[ \t]+", $def); - my $name = $a[0]; + my ($hash, $a, $h) = @_; + my $name = shift @$a; + my $type = shift @$a; my $mac; my $sshHost; $hash->{NAME} = $name; $hash->{STATE} = "initialized"; - $hash->{VERSION} = "1.0.0"; + $hash->{VERSION} = "2.0.0"; $hash->{loglevel} = 4; Log3 $hash, 3, "GFPROBT: G.F.Pro Eco Watering Bluetooth ".$hash->{VERSION}; - if (int(@a) > 4) { + if (int(@{$a}) > 2) { return 'GFPROBT: Wrong syntax, must be define GFPROBT '; - } elsif(int(@a) == 3) { - $mac = $a[2]; - $hash->{MAC} = $a[2]; - } elsif(int(@a) == 4) { - $mac = $a[2]; - $hash->{MAC} = $a[2]; - $attr{$name}{sshHost} = $a[3]; + } elsif(int(@{$a}) == 1) { + $mac = shift @$a; + $hash->{MAC} = $mac; + } elsif(int(@{$a}) == 2) { + $mac = shift @$a; + $hash->{MAC} = $mac; + $attr{$name}{sshHost} = shift @$a; } + $hash->{helper}{currenthcidevice} = -1; GFPROBT_updateHciDevicelist($hash); RemoveInternalTimer($hash); @@ -85,7 +89,10 @@ sub GFPROBT_updateHciDevicelist { push(@{$hash->{helper}{hcidevices}}, $1); } } - $hash->{helper}{currenthcidevice} = 0; + $hash->{helper}{currenthcidevice} += 1; + if ($hash->{helper}{currenthcidevice} >= int(@{$hash->{helper}{hcidevices}})) { + $hash->{helper}{currenthcidevice} = 0; + } readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1); return undef; } @@ -110,22 +117,37 @@ sub GFPROBT_Attribute($$$$) { return undef; } -sub GFPROBT_Set($@) { - my ($hash, $name, @params) = @_; - my $workType = shift(@params); - my $list = "on off"; +sub GFPROBT_Set($$$) { + my ($hash, $a, $h) = @_; + my $name = shift @$a; + my $workType = shift @$a; + my $list = "on off devicename addTimer deleteTimer editTimer eco adjust"; # check parameters for set function if($workType eq "?") { - return SetExtensions($hash, $list, $name, $workType, @params); + return SetExtensions($hash, $list, $name, $workType, $a); } if($workType eq "on") { - GFPROBT_setOn($hash); + if (int(@$a) == 0) { + GFPROBT_setOn($hash); + } else { + GFPROBT_setOnSeconds($hash, $a); + } } elsif($workType eq "off") { GFPROBT_setOff($hash); + } elsif($workType eq "devicename") { + GFPROBT_setDevicename($hash, $a); + } elsif($workType eq "addTimer") { + GFPROBT_addTimer($hash, $h); + } elsif($workType eq "deleteTimer") { + GFPROBT_deleteTimer($hash, $a); + } elsif($workType eq "editTimer") { + GFPROBT_editTimer($hash, $h); + } elsif($workType eq "adjust") { + GFPROBT_setAdjust($hash, $a); } else { - return SetExtensions($hash, $list, $name, $workType, @params); + return SetExtensions($hash, $list, $name, $workType, $a); } return undef; @@ -157,12 +179,6 @@ sub GFPROBT_updateStatusSuccessful { return undef; } -sub GFPROBT_updateStatusRetry { - my ($hash) = @_; - GFPROBT_updateStatus($hash); - return undef; -} - sub GFPROBT_updateStatusFailed { my ($hash) = @_; InternalTimer(gettimeofday()+170+int(rand(60)), "GFPROBT_updateStatus", $hash, 0); @@ -189,9 +205,97 @@ sub GFPROBT_setOnFailed { return undef; } -sub GFPROBT_setOnRetry { +### addTimer ### +sub GFPROBT_addTimer { + my ($hash, $h) = @_; + my $name = $hash->{NAME}; + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|addTimer|".$h->{'start'}."|".$h->{'duration'}."|".$h->{'weekdays'}, "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +### editTimer ### +sub GFPROBT_editTimer { + my ($hash, $h) = @_; + my $name = $hash->{NAME}; + readingsSingleUpdate($hash, "state", "sending", 1); + + if (!defined($h->{'timer'})) { + $h->{'timer'} = "all"; + } + if (!defined($h->{'start'})) { + $h->{'start'} = "-:-"; + } + if (!defined($h->{'duration'})) { + $h->{'duration'} = "-"; + } + if (!defined($h->{'weekdays'})) { + $h->{'weekdays'} = "-"; + } + + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|editTimer|".$h->{'timer'}."|".$h->{'start'}."|".$h->{'duration'}."|".$h->{'weekdays'}, "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +### deleteTimer ### +sub GFPROBT_deleteTimer { + my ($hash, $opt) = @_; + my $name = $hash->{NAME}; + my $timerNr = shift(@$opt); + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|deleteTimer|".$timerNr, "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +### deleteOnTimer ### +sub GFPROBT_deleteOnTimer { my ($hash) = @_; - GFPROBT_retryGatttool($hash, "setOn"); + my $name = $hash->{NAME}; + my $timer = $hash->{'deleteOnTimer'}; + delete($hash->{'deleteOnTimer'}); + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|deleteOnTimer|".$timer->{'hour'}."|".$timer->{'minute'}."|".$timer->{'duration'}, "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +### setAdjust ### +sub GFPROBT_setAdjust { + my ($hash, $opt) = @_; + my $name = $hash->{NAME}; + my $perc = shift(@$opt); + my $days = shift(@$opt); + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|setAdjust|".$perc."|".$days, "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +### setDevicename ### +sub GFPROBT_setDevicename { + my ($hash, $param) = @_; + my $name = $hash->{NAME}; + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|setDevicename|".@$param[0], "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); + return undef; +} + +sub GFPROBT_setDevicenameSuccessful { + my ($hash) = @_; + return undef; +} + +sub GFPROBT_setDevicenameFailed { + my ($hash) = @_; + readingsSingleUpdate($hash, "state", "failed", 1); + return undef; +} + +### setOnSeconds ### +sub GFPROBT_setOnSeconds { + my ($hash, $opt) = @_; + my $onseconds = shift @$opt; + my $name = $hash->{NAME}; + readingsSingleUpdate($hash, "state", "sending", 1); + $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $name."|".$hash->{MAC}."|setOnSeconds|$onseconds", "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); return undef; } @@ -206,7 +310,6 @@ sub GFPROBT_setOff { sub GFPROBT_setOffSuccessful { my ($hash) = @_; - return undef; } @@ -216,22 +319,10 @@ sub GFPROBT_setOffFailed { return undef; } -sub GFPROBT_setOffRetry { - my ($hash) = @_; - GFPROBT_retryGatttool($hash, "setOff"); - return undef; -} - ### Gatttool functions ### -sub GFPROBT_retryGatttool { - my ($hash, $workType) = @_; - $hash->{helper}{RUNNING_PID} = BlockingCall("GFPROBT_execGatttool", $hash->{NAME}."|".$hash->{MAC}."|$workType", "GFPROBT_processGatttoolResult", 300, "GFPROBT_killGatttool", $hash); - return undef; -} - sub GFPROBT_execGatttool($) { my ($string) = @_; - my ($name, $mac, $workType) = split("\\|", $string); + my ($name, $mac, $workType, @params) = split("\\|", $string); my $wait = 1; my $hash = $main::defs{$name}; my $sshHost = AttrVal($name,"sshHost","none"); @@ -251,7 +342,7 @@ sub GFPROBT_execGatttool($) { $hash->{gattProc} = Expect->spawn('gatttool -b '.$hash->{MAC}.' -i '.$hciDevice. ' -I'); $hash->{gattProc}->raw_pty(1); - $hash->{gattProc}->log_stdout(0); + #$hash->{gattProc}->log_stdout(0); while (!$ret and $retries < 10) { $hash->{gattProc}->send("connect\r"); @@ -260,7 +351,7 @@ sub GFPROBT_execGatttool($) { sleep(3); } $retries += 1; - if ($retries > 10) { + if (!$ret and $retries > 9) { $hash->{gattProc}->hard_close(); return "$name|$mac|error|$workType|failed to connect"; } @@ -269,166 +360,379 @@ sub GFPROBT_execGatttool($) { #write password $hash->{gattProc}->send("char-write-req 0x0048 313233343536\r"); $hash->{gattProc}->expect(5, "Characteristic value was written successfully"); - #read current state + + #read watering $hash->{gattProc}->send("char-read-hnd 0x0015\r"); $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); if ($ret) { $json{'watering'} = GFPROBT_convertHexToInt($hash, $hash->{gattProc}->exp_after()); - } - #read battery - $hash->{gattProc}->send("char-read-hnd 0x0039\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'batteryVoltage'} = GFPROBT_convertHexToIntReverse($hash, $hash->{gattProc}->exp_after(), 2); - if ($json{'batteryVoltage'} > 3575) { - $json{'battery'} = 100 - } else { - $json{'battery'} = int(($json{'batteryVoltage'} - 2900) / 6.75); - } - } - - #read temperature - $hash->{gattProc}->send("char-read-hnd 0x003b\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); - } - - #read min temperature - $hash->{gattProc}->send("char-read-hnd 0x003d\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'min-temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); - } - - #read max temperature - $hash->{gattProc}->send("char-read-hnd 0x003f\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'max-temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); - } - - #read firmware version - $hash->{gattProc}->send("char-read-hnd 0x004e\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'firmware'} = GFPROBT_getFirmware($hash, $hash->{gattProc}->exp_after()); - } - - #read device name - $hash->{gattProc}->send("char-read-hnd 0x0052\r"); - $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); - if ($ret) { - $json{'devicename'} = GFPROBT_convertHexToString($hash, $hash->{gattProc}->exp_after()); - } - - #read timers - foreach my $i ((1,2,3,4)) { - $json{"timer".$i."-Start"} = "-"; - $json{"timer".$i."-Duration"} = "-"; - $json{"timer".$i."-Weekdays"} = "-"; - } - my %timers; - my @timerHnd = ("0x0017", "0x0019", "0x001b", "0x001d", "0x001f", "0x0021", "0x0023", "0x0031", "0x0025", "0x0027", "0x0029", - "0x002b", "0x002d", "0x002f"); - my $stop = 0; - foreach my $hnd (@timerHnd) { - if ($stop == 1) { - last; - } - $hash->{gattProc}->send("char-read-hnd $hnd\r"); + #read battery + $hash->{gattProc}->send("char-read-hnd 0x0039\r"); $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); if ($ret) { - foreach my $i ((0,6)) { - my @hexarr = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after()); - my @hexarr2 = @hexarr[$i,$i+1,$i+2,$i+3,$i+4,$i+5]; - my ($weekday, $hour, $minute, $duration) = GFPROBT_convertHexToTimer($hash, \@hexarr2); - if (!defined($weekday)) { - $stop = 1; + $json{'batteryVoltage'} = GFPROBT_convertHexToIntReverse($hash, $hash->{gattProc}->exp_after()); + if ($json{'batteryVoltage'} > 3575) { + $json{'battery'} = 100 + } else { + $json{'battery'} = int(($json{'batteryVoltage'} - 2900) / 6.75); + } + } + + #read temperature + $hash->{gattProc}->send("char-read-hnd 0x003b\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + $json{'temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); + } + + #read min temperature + $hash->{gattProc}->send("char-read-hnd 0x003d\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + $json{'min-temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); + } + + #read max temperature + $hash->{gattProc}->send("char-read-hnd 0x003f\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + $json{'max-temperature'} = GFPROBT_convertHexToTemp($hash, $hash->{gattProc}->exp_after()); + } + + #read firmware version + $hash->{gattProc}->send("char-read-hnd 0x004e\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + $json{'firmware'} = GFPROBT_getFirmware($hash, $hash->{gattProc}->exp_after()); + } + + #read device name + $hash->{gattProc}->send("char-read-hnd 0x0052\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + $json{'devicename'} = GFPROBT_convertHexToString($hash, $hash->{gattProc}->exp_after()); + } + + #read complete status + #$hash->{gattProc}->send("char-read-hnd 0x0050\r"); + #$ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + #if ($ret) { + # $json{'status'} = GFPROBT_getHexString($hash, $hash->{gattProc}->exp_after()); + #} + + #read ECO1 + #$hash->{gattProc}->send("char-read-hnd 0x0033\r"); + #$ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + #if ($ret) { + # $json{'eco'} = GFPROBT_getHexString($hash, $hash->{gattProc}->exp_after()); + #} + + #read ECO2 + #$hash->{gattProc}->send("char-read-hnd 0x0045\r"); + #$ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + #if ($ret) { + # $json{'eco'} .= " ".GFPROBT_getHexString($hash, $hash->{gattProc}->exp_after()); + #} + + #read time offset + $hash->{gattProc}->send("char-read-hnd 0x0035\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + my @hexarr = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after()); + $json{'deviceTime'} = GFPROBT_convertHexArrToSeconds($hash, \@hexarr); + } + + #read MAC + $hash->{gattProc}->send("char-read-hnd 0x004a\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + my @mac = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after());; + $json{'mac'} = join(":", reverse(@mac)); + } + + #read timers + my %timers; + $hash->{'timerHnd'} = ["0x0017", "0x0019", "0x001b", "0x001d", "0x001f", "0x0021", "0x0023", "0x0031", "0x0025", "0x0027", "0x0029", + "0x002b", "0x002d", "0x002f"]; + my $stop = 0; + my $storageCnt = 0; + foreach my $hnd (@{$hash->{'timerHnd'}}) { + if ($stop == 1) { + last; + } + $hash->{gattProc}->send("char-read-hnd $hnd\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + foreach my $i ((0,6)) { + my @hexarr = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after()); + my @hexarr2 = @hexarr[$i,$i+1,$i+2,$i+3,$i+4,$i+5]; + my ($weekday, $hour, $minute, $duration) = GFPROBT_convertHexToTimer($hash, \@hexarr2); + if (!defined($weekday)) { + $stop = 1; + last; + } + $storageCnt += 1; + if (!exists($timers{$hour})) { + $timers{$hour} = { + $minute => { + $duration => [$weekday] + } + }; + } elsif (!exists($timers{$hour}{$minute})) { + $timers{$hour}{$minute} = { $duration => [$weekday] }; + } elsif (!exists($timers{$hour}{$minute}{$duration})) { + $timers{$hour}{$minute}{$duration} = [$weekday]; + } else { + push(@{$timers{$hour}{$minute}{$duration}}, $weekday); + } + } + } + } + + #write timers + if ($workType eq "addTimer") { + if ($storageCnt >= 28) { + #check timer storage (28max) + return "$name|$mac|error|$workType|max of 28 single timers reached"; + } + my $newhour = (split(":", $params[0]))[0]; + my $newminute = (split(":", $params[0]))[1]; + my $newduration = $params[1]; + my @newweekdays = (); + if (!defined($params[2])) { + @newweekdays = ("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"); + } else { + @newweekdays = split(",", $params[2]) + } + my %newtime = (hour=>$newhour, minute=>$newminute, duration=>$newduration, weekdays=>\@newweekdays); + GFPROBT_addTimerToTimers($hash, \%timers, \%newtime); + + GFPROBT_saveTimers($hash, \%timers); + + } elsif ($workType eq "editTimer") { + my $timernr = $params[0]; #nr or all + my $paramhour = (split(":", $params[1]))[0]; #hour or - if no change + my $paramminute = (split(":", $params[1]))[1]; #minute or - if no change + my $paramduration = $params[2]; #duration or - if no change + my @paramweekdays = split(",", $params[3]); #weekdays or - if no change + + my @nrArr = split(",", $timernr); + if ($params[0] eq "all") { + @nrArr = (1..28); + } + foreach my $i (@nrArr) { + my $delTime = ReadingsVal($hash->{NAME}, "timer".$i."-Start", 0); + if ($delTime eq "0") { last; } - if (!exists($timers{$hour})) { - $timers{$hour} = { - $minute => { - $duration => [$weekday] - } - }; - } elsif (!exists($timers{$hour}{$minute})) { - $timers{$hour}{$minute} = { $duration => [$weekday] }; - } elsif (!exists($timers{$hour}{$minute}{$duration})) { - $timers{$hour}{$minute}{$duration} = [$weekday]; + my $delHour = int((split(":", $delTime))[0]); + my $delMin = int((split(":", $delTime))[1]); + my $delDur = ReadingsVal($hash->{NAME}, "timer".$i."-Duration", 0); + my $delWeekdays = ReadingsVal($hash->{NAME}, "timer".$i."-Weekdays", "Mo,Tu,We,Th,Fr,Sa,Su"); + my $newhour = 0; + my $newminute = 0; + my $newduration = 0; + my @newweekdays = (); + + if ($paramhour eq "-") { + $newhour = $delHour; } else { - push(@{$timers{$hour}{$minute}{$duration}}, $weekday); + $newhour = $paramhour; + } + if ($paramminute eq "-") { + $newminute = $delMin; + } else { + $newminute = $paramminute; + } + if ($paramduration eq "-") { + $newduration = $delDur; + } else { + $newduration= $paramduration; + } + if ($paramweekdays[0] eq "-") { + @newweekdays = split(",", $delWeekdays); + } else { + @newweekdays = @paramweekdays; + } + delete($timers{$delHour}{$delMin}{$delDur}); + my %newtime = (hour=>$newhour, minute=>$newminute, duration=>$newduration, weekdays=>\@newweekdays); + GFPROBT_addTimerToTimers($hash, \%timers, \%newtime); + } + + GFPROBT_saveTimers($hash, \%timers); + } elsif ($workType eq "deleteTimer") { + my @nrArr = split(",", $params[0]); + if ($params[0] eq "all") { + @nrArr = (1..28); + } + foreach my $i (@nrArr) { + my $delTime = ReadingsVal($hash->{NAME}, "timer".$i."-Start", 0); + if ($delTime eq "0") { + last; + } + my $delHour = int((split(":", $delTime))[0]); + my $delMin = int((split(":", $delTime))[1]); + my $delDur = ReadingsVal($hash->{NAME}, "timer".$i."-Duration", 0); + delete($timers{$delHour}{$delMin}{$delDur}); + } + + + GFPROBT_saveTimers($hash, \%timers); + } elsif ($workType eq "deleteOnTimer") { + my $delHour = int($params[0]); + my $delMin = int($params[1]); + my $delDur = $params[2]; + delete($timers{$delHour}{$delMin}{$delDur}); + + GFPROBT_saveTimers($hash, \%timers); + } elsif ($workType eq "setEco") { + #localtime->week + + } elsif ($workType eq "setAdjust") { + my $percHex = GFPROBT_convertIntToHexReverse($hash, $params[0]); + my $daysHex = GFPROBT_convertIntToHexReverse($hash, $params[1]*86400); + $percHex = substr($percHex, 0, 4); + my $hex = $daysHex.$percHex; + + $hash->{gattProc}->send("char-write-req 0x0043 $hex\r"); + $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); + + GFPROBT_commitCode($hash); + + } elsif ($workType eq "setOnSeconds") { + if ($storageCnt >= 28) { + #check timer storage (28max) + return "$name|$mac|error|$workType|max of 28 single timers reached"; + } + my $onseconds = $params[0]; + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + my @day = ("Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"); + #onseconds+1 to reduce the chance of duplicate timers + my @onwday = ($day[$wday]); + my %newtime = (hour=>$hour, minute=>$min, duration=>$sec+$onseconds+1, weekdays=>\@onwday); + GFPROBT_addTimerToTimers($hash, \%timers, \%newtime); + + GFPROBT_saveTimers($hash, \%timers); + + #delete timer when finished (call function after $duration+3 min) + $json{'DATA'} = { + 'deleteOnTimer' => { + 'hour' => $hour, + 'minute' => $min, + 'duration' => $sec+$onseconds+1 + } + }; + } elsif ($workType eq "setDevicename") { + my $devname = $params[0]; + for (my $i=length($devname); $i<20; $i++) { + $devname .= " "; + } + my $devnameHex = GFPROBT_convertStringToHex($hash, $devname); + $hash->{gattProc}->send("char-write-req 0x0052 $devnameHex\r"); + $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); + } elsif ($workType eq "setOff" or $workType eq "setOn") { + if (($json{'watering'} == 1 and $workType eq "setOff") or + ($json{'watering'} == 0 and $workType eq "setOn")) { + #switch on/off + $hash->{gattProc}->send("char-write-req 0x0013 00\r"); + $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); + $hash->{gattProc}->send("char-write-req 0x0013 01\r"); + $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); + + #read current state + $hash->{gattProc}->send("char-read-hnd 0x0015\r"); + $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + $json{'watering'} = GFPROBT_convertHexToInt($hash, $hash->{gattProc}->exp_after()); + } + } + + #read timers again + %timers = (); + $stop = 0; + $storageCnt = 0; + foreach my $hnd (@{$hash->{'timerHnd'}}) { + if ($stop == 1) { + last; + } + $hash->{gattProc}->send("char-read-hnd $hnd\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + foreach my $i ((0,6)) { + my @hexarr = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after()); + my @hexarr2 = @hexarr[$i,$i+1,$i+2,$i+3,$i+4,$i+5]; + my ($weekday, $hour, $minute, $duration) = GFPROBT_convertHexToTimer($hash, \@hexarr2); + if (!defined($weekday)) { + $stop = 1; + last; + } + $storageCnt += 1; + if (!exists($timers{$hour})) { + $timers{$hour} = { + $minute => { + $duration => [$weekday] + } + }; + } elsif (!exists($timers{$hour}{$minute})) { + $timers{$hour}{$minute} = { $duration => [$weekday] }; + } elsif (!exists($timers{$hour}{$minute}{$duration})) { + $timers{$hour}{$minute}{$duration} = [$weekday]; + } else { + push(@{$timers{$hour}{$minute}{$duration}}, $weekday); + } } } } - } - my $timercnt = 1; - foreach my $hour (keys %timers) { - foreach my $minute (keys %{$timers{$hour}}) { - foreach my $duration (keys %{$timers{$hour}{$minute}}) { - $json{'timer'.$timercnt."-Start"} = sprintf("%02d:%02d", $hour, $minute); - $json{'timer'.$timercnt."-Duration"} = int($duration/60); - $json{'timer'.$timercnt."-Weekdays"} = join(",", @{$timers{$hour}{$minute}{$duration}}); - $timercnt += 1; + my $timercnt = 1; + foreach my $hour (sort { $a <=> $b } keys %timers) { + foreach my $minute (sort { $a <=> $b } keys %{$timers{$hour}}) { + foreach my $duration (sort { $a <=> $b } keys %{$timers{$hour}{$minute}}) { + $json{'timer'.$timercnt."-Start"} = sprintf("%02d:%02d", $hour, $minute); + $json{'timer'.$timercnt."-Duration"} = int($duration); + $json{'timer'.$timercnt."-Weekdays"} = join(",", @{$timers{$hour}{$minute}{$duration}}); + $timercnt += 1; + } } } - } - - #write timers - if ($workType eq "addTimer") { - my $newhour; - my $newminute; - my $newduration; - my @newweekdays; - if ($timercnt > 4) { - return "$name|$mac|error|$workType|limit of 4 timers reached, delete one timer first"; + + #read increase/reduce + $hash->{gattProc}->send("char-read-hnd 0x0043\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { + my @hexarr = GFPROBT_getHexOutput($hash, $hash->{gattProc}->exp_after()); + my @hextime = @hexarr[0,1,2,3]; + my @hexpercentage = @hexarr[4,5]; + my $perc = GFPROBT_convertHexArrToIntReverse($hash, \@hexpercentage); + if ($perc > 65536/2) { + $perc -= 65536; } - if (!exists($timers{$newhour})) { - $timers{$newhour} = { - $newminute = { - $newduration => @newweekdays - } - }; - } elsif (!exists($timers{$newhour})) { - $timers{$newhour}{$newminute} = { - $newduration => @newweekdays - }; - } elsif (!exists($timers{$newhour}{$newminute}{$newduration})) { - $timers{$newhour}{$newminute}{$newduration} = @newweekdays; + $json{'adjustPercentage'} = $perc; + if ($json{'adjustPercentage'} !=0 ) { + my $seconds = GFPROBT_convertHexArrToSeconds($hash, \@hextime); + my $till = time() + $seconds; + $json{'adjustTill'} = sprintf("%s", scalar localtime($till)); } else { - push(@{$timers{$newhour}{$newminute}{$newduration}}, @newweekdays); + $json{'adjustTill'} = "-"; } - - #writeOffset - #commitCode - } - - if (($json{'watering'} == 1 and $workType eq "setOff") or - ($json{'watering'} == 0 and $workType eq "setOn")) { - #switch on/off - $hash->{gattProc}->send("char-write-req 0x0013 00\r"); - $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); - $hash->{gattProc}->send("char-write-req 0x0013 01\r"); - $hash->{gattProc}->expect(2, "Characteristic value was written successfully"); - - #read current state - $hash->{gattProc}->send("char-read-hnd 0x0015\r"); - $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + } + + #re-read watering + $hash->{gattProc}->send("char-read-hnd 0x0015\r"); + $ret = $hash->{gattProc}->expect(2, "Characteristic value/descriptor: "); + if ($ret) { $json{'watering'} = GFPROBT_convertHexToInt($hash, $hash->{gattProc}->exp_after()); - } - - if (defined($json{'watering'})) { + } + if ($json{'watering'} == 1) { $json{'state'} = 'on'; } else { $json{'state'} = 'off'; } + + $hash->{gattProc}->send("disconnect\r"); + $hash->{gattProc}->send("exit\r"); } - - $hash->{gattProc}->send("disconnect"); - $hash->{gattProc}->send("exit"); + $hash->{gattProc}->hard_close(); my $jsonString = encode_json \%json; @@ -439,6 +743,138 @@ sub GFPROBT_execGatttool($) { } } +sub GFPROBT_saveTimers($$) { + my ($hash, $timers) = @_; + + #writeTimers + GFPROBT_writeTimers($hash, $timers); + + #writeOffset + GFPROBT_writeOffset($hash); + + #commitCode + GFPROBT_commitCode($hash); + + #write statusbyte + #GFPROBT_writeStatusByte($hash); +} + +sub GFPROBT_writeTimers($$) { + my ($hash, $timers) = @_; + + my %dayToNr = ( + 'Mo' => 0, + 'Tu' => 1, + 'We' => 2, + 'Th' => 3, + 'Fr' => 4, + 'Sa' => 5, + 'Su' => 6 + ); + my %startDuration; + my @startTime; + foreach my $hour (sort keys %{$timers}) { + foreach my $minute (sort keys %{$timers->{$hour}}) { + foreach my $duration (sort keys %{$timers->{$hour}{$minute}}) { + foreach my $day (@{$timers->{$hour}{$minute}{$duration}}) { + my $startT = $dayToNr{$day}*3600*24+$hour*3600+$minute*60; + $startDuration{$startT} = $duration; + push(@startTime, $startT); + } + } + } + } + + @startTime = sort { $a <=> $b } @startTime; + + my @timerPairs = (); + my $hexString = ""; + foreach my $startT (@startTime) { + #calculate seconds and convert to hex + my $secFromMondayHex = GFPROBT_convertIntToHexReverse($hash, $startT); + #add duration + my $durationHex = GFPROBT_convertIntToHexReverse($hash, $startDuration{$startT}); + $durationHex = substr($durationHex, 0, 4); + $hexString .= $secFromMondayHex.$durationHex; + if (length($hexString) > 12) { + push(@timerPairs, $hexString); + Log3 $hash, 3, "Hex: ".$hexString; + $hexString = ""; + } + } + + if (length($hexString) > 0) { + $hexString .= "ffffffff0000"; + push(@timerPairs, $hexString); + } + + #write other timer HNDs FF FF FF FF 00 00 + for (my $i=@timerPairs; $i<14; $i++) { + push(@timerPairs, "ffffffff0000ffffffff0000"); + } + + #write 2 timers to 1 HND + my $i = 0; + foreach my $timer(@timerPairs) { + my $hnd = @{$hash->{'timerHnd'}}[$i]; + $hash->{gattProc}->send("char-write-req $hnd $timer\r"); + $hash->{gattProc}->expect(3, "Characteristic value was written successfully"); + $i += 1; + } +} + +sub GFPROBT_addTimerToTimers($$$) { + my ($hash, $timers, $newtimer) = @_; + + if (!exists($timers->{$newtimer->{'hour'}})) { + $timers->{$newtimer->{'hour'}} = { + $newtimer->{'minute'} => { + $newtimer->{'duration'} => $newtimer->{'weekdays'} + } + }; + } elsif (!exists($timers->{$newtimer->{'hour'}}{$newtimer->{'minute'}})) { + $timers->{$newtimer->{'hour'}}{$newtimer->{'minute'}} = { + $newtimer->{'duration'} => $newtimer->{'weekdays'} + }; + } elsif (!exists($timers->{$newtimer->{'hour'}}{$newtimer->{'minute'}}{$newtimer->{'duration'}})) { + $timers->{$newtimer->{'hour'}}{$newtimer->{'minute'}}{$newtimer->{'duration'}} = $newtimer->{'weekdays'}; + } else { + push(@{$timers->{$newtimer->{'hour'}}{$newtimer->{'minute'}}{$newtimer->{'duration'}}}, $newtimer->{'weekdays'}); + } +} + +sub GFPROBT_writeOffset($) { + my ($hash) = @_; + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + $wday -= 1; + if ($wday < 0) { + $wday = 6; + } + my $secFromMonday = $wday*3600*24 + $hour*3600 + $min*60 + $sec; + my $secFromMondayHex = GFPROBT_convertIntToHexReverse($hash, $secFromMonday); + $hash->{gattProc}->send("char-write-req 0x0035 $secFromMondayHex\r"); + $hash->{gattProc}->expect(10, "Characteristic value was written successfully"); +} + +sub GFPROBT_commitCode($) { + my ($hash) = @_; + $hash->{gattProc}->send("char-write-req 0x0037 00\r"); + $hash->{gattProc}->expect(5, "Characteristic value was written successfully"); + $hash->{gattProc}->send("char-write-req 0x0037 01\r"); + $hash->{gattProc}->expect(5, "Characteristic value was written successfully"); +} + +sub GFPROBT_writeStatusByte($) { + my ($hash) = @_; + $hash->{gattProc}->send("char-write-req 0x0050 0000000000000000000000000000000000000000\r"); + $hash->{gattProc}->expect(5, "Characteristic value was written successfully"); +} + +sub GFPROBT_convertIntToHexReverse($$) { + my ($hash, $input) = @_; + return unpack "H*", pack "I", $input; +} + sub GFPROBT_getHexOutput($$) { my ($hash, $input) = @_; my $val; @@ -455,10 +891,18 @@ sub GFPROBT_getFirmware($$) { return hex("0x".$hexarr[1]).".".hex("0x".$hexarr[0]); } +sub GFPROBT_getHexString($$) { + my ($hash, $input) = @_; + my @hexarr = GFPROBT_getHexOutput($hash, $input); + + return join(" ", @hexarr); +} + sub GFPROBT_convertHexToTimer($$) { my ($hash, $input) = @_; my @hexarr = @{$input}; - my $seconds = unpack "I", pack "H*", join("", @hexarr[0,1,2,3]); + my @hextime = @hexarr[0,1,2,3]; + my $seconds = GFPROBT_convertHexArrToSeconds($hash, \@hextime); if ($seconds == 4294967295) { #FF FF FF FF return (undef, undef, undef, undef); @@ -467,12 +911,18 @@ sub GFPROBT_convertHexToTimer($$) { my $weekday = $day[int($seconds/(3600*24))]; my $hour = int(($seconds%(3600*24))/3600); my $minutes = int(($seconds%(3600*24) - $hour*3600) / 60); - Log3 $hash, 3, "Seconds from Monday: ".$seconds.", ".$weekday."\n"; $seconds = $seconds%60; my $duration = unpack "I", pack "H*", join("", @hexarr[4,5])."0000"; return ($weekday, $hour, $minutes, $duration); } +sub GFPROBT_convertHexArrToSeconds($$) { + my ($hash, $input) = @_; + my @hexarr = @{$input}; + my $seconds = unpack "I", pack "H*", join("", @hexarr); + return $seconds; +} + sub GFPROBT_convertHexToTemp($$) { my ($hash, $input) = @_; my @hexarr = GFPROBT_getHexOutput($hash, $input); @@ -480,6 +930,11 @@ sub GFPROBT_convertHexToTemp($$) { return hex("0x".$hexarr[0]) + hex("0x".$hexarr[1])/100; } +sub GFPROBT_convertStringToHex($$) { + my ($hash, $input) = @_; + return unpack "H*", $input; +} + sub GFPROBT_convertHexToString($$) { my ($hash, $input) = @_; my @hexarr = GFPROBT_getHexOutput($hash, $input); @@ -496,14 +951,25 @@ sub GFPROBT_convertHexToInt($$) { return hex("0x".$val); } -sub GFPROBT_convertHexToIntReverse($$$) { - my ($hash, $input, $length) = @_; +sub GFPROBT_convertHexArrToIntReverse($$) { + my ($hash, $input) = @_; + my @hexarr = @{$input}; + + for (my $length=@hexarr; $length<4; $length++) { + push @hexarr, "00"; + } + + return unpack "I", pack "H*", join("",@hexarr); +} + +sub GFPROBT_convertHexToIntReverse($$) { + my ($hash, $input) = @_; my $val; if ($input =~ /(.*)$/m) { $val = $1; } $val =~ s/\s//g; - for (;$length <= 4; $length++) { + for (my $length = length($val); $length < 8; $length++) { $val = $val."0"; } $val = unpack "I", pack "H*", $val; @@ -543,45 +1009,18 @@ sub GFPROBT_processGatttoolResult($) { &{$call}($hash); }; use strict "refs"; - RemoveInternalTimer($hash, "GFPROBT_".$workType."Retry"); - $hash->{helper}{"retryCounter$workType"} = 0; } else { - $hash->{helper}{"retryCounter$workType"} = 0 if(!defined($hash->{helper}{"retryCounter$workType"})); - $hash->{helper}{"retryCounter$workType"}++; - Log3 $hash, 4, "GFPROBT ($name): $workType failed ($json)"; - if ($hash->{helper}{"retryCounter$workType"} > AttrVal($name, "maxRetries", 20)) { - my $errorCount = ReadingsVal($hash->{NAME}, "errorCount-$workType", 0); - readingsSingleUpdate($hash, "errorCount-$workType", $errorCount+1, 1); - Log3 $hash, 3, "GFPROBT ($name): $workType, failed 20 times."; - $hash->{helper}{"retryCounter$workType"} = 0; - $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0; - #call WorkTypeFailed function - my $call = "GFPROBT_".$workType."Failed"; - no strict "refs"; - eval { - &{$call}($hash); - }; - use strict "refs"; - - #update hci devicelist - GFPROBT_updateHciDevicelist($hash); - } else { - $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0 if(!defined($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}})); - $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}}++; - if ($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} > 7) { - #reset error counter - $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0; - #use next hci device next time - $hash->{helper}{currenthcidevice} += 1; - my $maxHciDevices = @{ $hash->{helper}{hcidevices} } - 1; - if($hash->{helper}{currenthcidevice} > $maxHciDevices) { - $hash->{helper}{currenthcidevice} = 0; - } - #update reading - readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1); - } - InternalTimer(gettimeofday()+3+int(rand(5)), "GFPROBT_".$workType."Retry", $hash, 0); - } + #call WorkTypeFailed function + readingsSingleUpdate($hash, 'errorCount-'.$workType, ReadingsVal($hash->{NAME}, 'errorCount-'.$workType, 0) + 1, 1); + my $call = "GFPROBT_".$workType."Failed"; + no strict "refs"; + eval { + &{$call}($hash); + }; + use strict "refs"; + + #update hci devicelist + GFPROBT_updateHciDevicelist($hash); } return undef; @@ -592,10 +1031,27 @@ sub GFPROBT_processJson { my $dataref = decode_json($json); my %data = %$dataref; - foreach my $reading (keys %data) { - readingsSingleUpdate($hash, $reading, $data{$reading}, 1); + readingsBeginUpdate($hash); + foreach my $i (1..28) { + readingsDelete($hash, "timer".$i."-Start"); + readingsDelete($hash, "timer".$i."-Duration"); + readingsDelete($hash, "timer".$i."-Weekdays"); } - + foreach my $reading (keys %data) { + if ($reading ne "DATA") { + readingsBulkUpdate($hash, $reading, $data{$reading}); + } + } + readingsEndUpdate($hash, 1); + + #start timer for timer deletion + if (defined($data{"DATA"})) { + if (defined($data{"DATA"}{"deleteOnTimer"})) { + $hash->{'deleteOnTimer'} = $data{"DATA"}{"deleteOnTimer"}; + InternalTimer(gettimeofday()+$data{"DATA"}{"deleteOnTimer"}{"duration"}+10, "GFPROBT_deleteOnTimer", $hash, 0); + } + } + return undef; } @@ -646,7 +1102,7 @@ sub GFPROBT_Get($$) {
sudo apt install libio-tty-perl bluez
- sudo cpanm install Expect + sudo cpanm Expect DateTime

Note: GFPro LED must blink during define! Please check if gatttool executable is available on your system. @@ -673,8 +1129,13 @@ sub GFPROBT_Get($$) { set <name> <command> [<parameter>]
The following commands are defined:

    -
  • on   -   switch on watering
  • +
  • on   [seconds]   switch on watering, optional for X seconds
  • off   -   switch off watering
  • +
  • addTimer   duration=300 start=8:00 weekdays=Mo,Tu,We,Th,Fr,Sa,Su   add timer with duration in seconds, starttime and weekdays (default all weekdays)
  • +
  • editTimer   timer=1 duration=300 start=8:00 weekdays=Mo,Fr   update timer 1 to duration, start, weekdays. Parameters not provided will remain unchanged
  • +
  • deleteTimer   number or all   delete one, more or all timers
  • +
  • adjust   percentage days   adjust percentage for x days
  • +
  • devicename   name   set devicename