change basis code, add little DevIo Helper

This commit is contained in:
Marko Oldenburg 2018-10-19 15:42:04 +02:00
parent b22e96756d
commit d9c6bb5f24

View File

@ -3,7 +3,8 @@
# Copyright notice
#
# (c) 2016
# Copyright: Juergen Kellerer (juergen at k123 dot eu)
# Copyright: Juergen Kellerer (juergen at k123 dot eu)
# FHEM Maintenance: Marko Oldenburg (leongaultier at gmail dot com)
# All rights reserved
#
# This script free software; you can redistribute it and/or modify
@ -38,10 +39,13 @@
# - https://github.com/Tristan79/iBrew
# .. and to all the volonteers crafting the FHEM project.
#
# Version: 0.9.1
# Version: 1.0.0
#
#############################################################
#
# v1.0.0 - 2018-10-19
# - change modul code to package module routine
# - change code FHEM conform
# - add multiple Attribut to control IoDev modul output
# v0.9.1 - 2017-04-25
# - fixed "stop" detection interferring with "extra" strength.
# - added new state "grinding".
@ -106,20 +110,86 @@ package main;
use strict;
use warnings;
my $version = "1.0.0";
sub SmarterCoffee_Initialize($) {
my ($hash) = @_;
$hash->{DefFn} = 'SmarterCoffee::Define';
$hash->{UndefFn} = 'SmarterCoffee::Undefine';
$hash->{GetFn} = 'SmarterCoffee::Get';
$hash->{SetFn} = 'SmarterCoffee::Set';
$hash->{ReadFn} = 'SmarterCoffee::Read';
$hash->{ReadyFn} = 'SmarterCoffee::OpenIfRequiredAndWritePending';
$hash->{NotifyFn} = 'SmarterCoffee::Notify';
$hash->{AttrFn} = 'SmarterCoffee::Attr';
$hash->{AttrList} = ""
."default-hotplate-on-for-minutes "
."ignore-max-cups "
."set-on-brews-coffee "
."strength-coffee-weights "
."strength-extra-percent "
."strength-extra-pre-brew-cups "
."strength-extra-pre-brew-delay-seconds "
."strength-extra-start-on-device-strength:off,weak,medium,strong "
."devioLoglevel:0,1,2,3,4,5 "
.$readingFnAttributes;
foreach my $d ( sort keys %{ $modules{SmarterCoffee}{defptr} } ) {
my $hash = $modules{SmarterCoffee}{defptr}{$d};
$hash->{VERSION} = $version;
}
}
package SmarterCoffee;
use strict;
use warnings;
use POSIX;
use GPUtils qw(:all)
; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
use Data::Dumper;
use Socket;
use IO::Select;
use DevIo;
use HttpUtils;
#use HttpUtils;
my $SmarterCoffee_Port = 2081;
my $SmarterCoffee_DiscoveryInterval = 60 * 15;
my $SmarterCoffee_StrengthExtraDefaultPercent = 1.4;
my $SmarterCoffee_StrengthDefaultWeights = "3.5 3.9 4.3";
my %SmarterCoffee_Hotplate = (default => 15, min => 5, max => 40);
## Import der FHEM Funktionen
BEGIN {
GP_Import(
qw(readingsSingleUpdate
readingsBulkUpdate
readingsBeginUpdate
readingsEndUpdate
CommandAttr
defs
modules
Log3
AttrVal
ReadingsVal
ReadingsNum
Value
IsDisabled
deviceEvents
init_done
gettimeofday
InternalTimer
RemoveInternalTimer
DoTrigger)
);
}
my %SmarterCoffee_MessageMaps = (
my $port = 2081;
my $discoveryInterval = 60 * 15;
my $strengthExtraDefaultPercent = 1.4;
my $strengthDefaultWeights = "3.5 3.9 4.3";
my %hotplate = (default => 15, min => 5, max => 40);
my %messageMaps = (
status_bitmasks => [
# BIT 1 = ???
# BIT 2 = hotplate
@ -167,7 +237,7 @@ my %SmarterCoffee_MessageMaps = (
}
);
my %SmarterCoffee_Commands = (
my %commands = (
reset => "107e",
brew => "377e",
brew_with_settings => "33########7e",
@ -185,9 +255,9 @@ my %SmarterCoffee_Commands = (
history => "467e"
);
my @SmarterCoffee_GetCommands = ("info", "carafe_required_status", "cups_single_mode_status", "get_defaults"); #, "history"
my @getCommands = ("info", "carafe_required_status", "cups_single_mode_status", "get_defaults"); #, "history"
my %SmarterCoffee_ResponseCodes = (
my %responseCodes = (
'00' => { message => 'Ok', success => 'yes' },
'01' => { message => 'Ok, brewing in progress', success => 'yes' },
@ -200,7 +270,7 @@ my %SmarterCoffee_ResponseCodes = (
);
sub SmarterCoffee_ParseMessage {
sub ParseMessage {
my ($hash, $message) = @_;
$message = ($hash->{PARTIAL} // "") if (not defined($message));
@ -214,7 +284,7 @@ sub SmarterCoffee_ParseMessage {
if (int(@messages) > 1) {
my $failed;
for (@messages) {
$failed = 1 if (not SmarterCoffee_ParseMessage($hash, $_."7e"));
$failed = 1 if (not ParseMessage($hash, $_."7e"));
}
return not $failed;
}
@ -224,16 +294,15 @@ sub SmarterCoffee_ParseMessage {
# Parse response of a command.
if ($message =~ /^03([0-9a-f]{2})7e.*/) {
if (my $response = ($SmarterCoffee_ResponseCodes{$1} // 0)) {
SmarterCoffee_UpdateReadings($hash,
if (my $response = ($responseCodes{$1} // 0)) {
UpdateReadings($hash,
sub($) {
my ($updateReading) = @_;
while (my ($key, $value) = each %{$response}) {
$updateReading->( "last_command_$key", $value );
}
$updateReading->( "last_command", $hash->{".last_set_command"} );
}, 1
, 1);
}, 1);
} else {
Log3 $hash->{NAME}, 3, "Connection :: Unknown command response '$message'.";
}
@ -243,7 +312,7 @@ sub SmarterCoffee_ParseMessage {
if ($message =~ /^47([0-9a-f]{2})(.+)7e.*/) {
my @history = split("7d", $2);
Log 2, Dumper(@history); #TODO
Log3 $hash->{NAME}, 5, Dumper(@history); #TODO
}
# Parse default settings message.
@ -255,19 +324,19 @@ sub SmarterCoffee_ParseMessage {
hotplate => substr($1, 6, 2),
);
SmarterCoffee_ParseStatusValues($hash, \%values);
ParseStatusValues($hash, \%values);
DoTrigger($hash->{NAME}, "get_defaults");
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "defaults" ]});
Set($hash, @{[ $hash->{NAME}, "defaults" ]});
}
# Parse carafe detection status message.
if ($message =~ /^4d([0-9a-f]{2})7e.*/) {
SmarterCoffee_UpdateReading($hash, "carafe_required", ($1 eq "01" ? "no" : "yes"));
UpdateReading($hash, "carafe_required", ($1 eq "01" ? "no" : "yes"));
}
# Parse single cup mode status message.
if ($message =~ /^50([0-9a-f]{2})7e.*/) {
SmarterCoffee_UpdateReading($hash, "cups_single_mode", ($1 eq "00" ? "no" : "yes"));
UpdateReading($hash, "cups_single_mode", ($1 eq "00" ? "no" : "yes"));
}
# Parse info & discovery message.
@ -287,7 +356,7 @@ sub SmarterCoffee_ParseMessage {
cups => '0'.substr($message, 11, 1),
);
SmarterCoffee_ParseStatusValues($hash, \%values);
ParseStatusValues($hash, \%values);
}
$hash->{CONNECTION} = ""
@ -301,14 +370,14 @@ sub SmarterCoffee_ParseMessage {
return 0;
}
sub SmarterCoffee_DumpToExpression($) {
sub DumpToExpression($) {
my $d = Dumper($_[0]);
$d =~ s/\s+/ /g;
$d =~ s/[^\}]*(\{.+\})[^\}]*/$1/;
return $d;
}
sub SmarterCoffee_ParseStatusValues {
sub ParseStatusValues {
my ($hash, $values) = @_;
while (my ($mappingKey, $rawValue) = each %{$values}) {
@ -317,20 +386,20 @@ sub SmarterCoffee_ParseStatusValues {
my $unpackedStatusBits = sprintf('%08b', ord(pack("H2", $rawValue)));
$hash->{".last_status"} .= " ($unpackedStatusBits)";
for (@{$SmarterCoffee_MessageMaps{"status_bitmasks"}}) {
for (@{$messageMaps{"status_bitmasks"}}) {
my ($unpackedBitmask, $statusInfo) = @{$_};
my $bitmask = ord(pack("B8", $unpackedBitmask));
if (($bitmask & ord(pack("B8", $unpackedStatusBits))) == $bitmask) {
while (my ($k, $v) = each(%{$statusInfo})) { $status{$k} = $v }
Log3 $hash->{NAME}, 5, "Connection :: Matched all bits of $unpackedBitmask in $unpackedStatusBits. Setting: ".SmarterCoffee_DumpToExpression($statusInfo);
Log3 $hash->{NAME}, 5, "Connection :: Matched all bits of $unpackedBitmask in $unpackedStatusBits. Setting: ".DumpToExpression($statusInfo);
}
}
$values->{$mappingKey} = { %status };
} else {
if (defined($SmarterCoffee_MessageMaps{$mappingKey}{$rawValue})) {
$values->{$mappingKey} = $SmarterCoffee_MessageMaps{$mappingKey}{$rawValue};
if (defined($messageMaps{$mappingKey}{$rawValue})) {
$values->{$mappingKey} = $messageMaps{$mappingKey}{$rawValue};
} elsif ($mappingKey eq "hotplate") {
$values->{$mappingKey} = { "hotplate_on_for_minutes" => hex($rawValue) };
} else {
@ -342,7 +411,7 @@ sub SmarterCoffee_ParseStatusValues {
Log3 $hash->{NAME}, 5, "Connection :: Parsed message: ".Dumper($values);
SmarterCoffee_UpdateReadings($hash,
UpdateReadings($hash,
sub($) {
my ($updateReading) = @_;
my $state = 0;
@ -384,7 +453,7 @@ sub SmarterCoffee_ParseStatusValues {
);
}
sub SmarterCoffee_Connect($) {
sub Connect($) {
my ($hash) = @_;
my $isNewConnection = ReadingsVal($hash->{NAME},'state','none') eq "initializing";
@ -393,52 +462,52 @@ sub SmarterCoffee_Connect($) {
delete $hash->{INVALID_DEVICE} if defined($hash->{INVALID_DEVICE});
if ($hash->{AUTO_DETECT}) {
SmarterCoffee_RunDiscoveryProcess($hash, 1);
RunDiscoveryProcess($hash, 1);
}
if (defined($hash->{DeviceName})) {
if (not ($hash->{DeviceName} =~ m/^(.+):([0-9]+)$/)) {
$hash->{DeviceName} .= ":$SmarterCoffee_Port";
$hash->{DeviceName} .= ":$port";
}
DevIo_CloseDev($hash) if DevIo_IsOpen($hash);
main::main::DevIo_CloseDev($hash) if main::DevIo_IsOpen($hash);
delete $hash->{DevIoJustClosed} if ($hash->{DevIoJustClosed});
return SmarterCoffee_OpenIfRequiredAndWritePending($hash, $isNewConnection);
return OpenIfRequiredAndWritePending($hash, $isNewConnection);
}
return 0;
}
sub SmarterCoffee_OpenIfRequiredAndWritePending($;$) {
sub OpenIfRequiredAndWritePending($;$) {
my ($hash, $initial) = @_;
return DevIo_OpenDev($hash, ($initial ? 0 : 1), "SmarterCoffee_WritePending");
return main::DevIo_OpenDev($hash, ($initial ? 0 : 1), "SmarterCoffee::WritePending");
}
sub SmarterCoffee_HandleInitialConnectState($) {
sub HandleInitialConnectState($) {
my ($hash) = @_;
return if ($hash->{".initial-connection-state"});
if (DevIo_IsOpen($hash) and (ReadingsVal($hash->{NAME},'state','none') eq "disconnected" or ReadingsVal($hash->{NAME},'state','none') eq "opened")) {
if (main::DevIo_IsOpen($hash) and (ReadingsVal($hash->{NAME},'state','none') eq "disconnected" or ReadingsVal($hash->{NAME},'state','none') eq "opened")) {
$hash->{".initial-connection-state"} = 1;
readingsSingleUpdate($hash,'state','connected',0);
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "info" ]}) if (not $hash->{AUTO_DETECT});
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "carafe_required_status" ]});
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "cups_single_mode_status" ]});
Get($hash, @{[ $hash->{NAME}, "info" ]}) if (not $hash->{AUTO_DETECT});
Get($hash, @{[ $hash->{NAME}, "carafe_required_status" ]});
Get($hash, @{[ $hash->{NAME}, "cups_single_mode_status" ]});
delete $hash->{".initial-connection-state"};
}
}
sub SmarterCoffee_WritePending {
sub WritePending {
my ($hash, $mustSucceed) = @_;
if (DevIo_IsOpen($hash)) {
if (main::DevIo_IsOpen($hash)) {
my $pending = ($hash->{PENDING_COMMAND} // 0);
# Handling initial call on a fresh connection
SmarterCoffee_HandleInitialConnectState($hash);
HandleInitialConnectState($hash);
# Processing pending commands
if (($hash->{INVALID_DEVICE} // "0") eq "1") {
@ -448,14 +517,14 @@ sub SmarterCoffee_WritePending {
delete $hash->{PENDING_COMMAND} if defined($hash->{PENDING_COMMAND});
Log3 $hash->{NAME}, 4, "Connection :: Sending to ".$hash->{DeviceName}.": $pending";
DevIo_SimpleWrite($hash, $pending, 1);
main::DevIo_SimpleWrite($hash, $pending, 1);
$hash->{".raw_last_status"} = "";
my $result = DevIo_SimpleReadWithTimeout($hash, 5);
my $result = main::DevIo_SimpleReadWithTimeout($hash, 5);
if ($result) {
$result = SmarterCoffee_Read($hash, $result);
$result = Read($hash, $result);
} else {
DevIo_Disconnected($hash);
main::DevIo_Disconnected($hash);
}
$hash->{INVALID_DEVICE} = "1" if ($mustSucceed and not $result);
@ -467,11 +536,11 @@ sub SmarterCoffee_WritePending {
return undef;
}
sub SmarterCoffee_Read($;$) {
sub Read($;$) {
my ($hash, $buffer) = @_;
# Handle case that fhem reconnected a broken connection and state is "opened".
SmarterCoffee_HandleInitialConnectState($hash) if (not defined($buffer));
HandleInitialConnectState($hash) if (not defined($buffer));
# Abort read if we already detected that the device is invalid.
return 0 if ($hash->{INVALID_DEVICE} // 0);
@ -480,7 +549,7 @@ sub SmarterCoffee_Read($;$) {
$hash->{PARTIAL} = "" if (not defined($hash->{PARTIAL}) or defined($buffer) or length($hash->{PARTIAL} // "") >= 512);
# Reading available bytes from the socket (if not specified from external).
$buffer = DevIo_SimpleRead($hash) if (not defined($buffer));
$buffer = main::DevIo_SimpleRead($hash) if (not defined($buffer));
return 0 if (not defined($buffer));
# Appending message bytes as hex string.
@ -488,7 +557,7 @@ sub SmarterCoffee_Read($;$) {
# Parsing the message and populate readings.
if ($hash->{PARTIAL} ne "") {
if (SmarterCoffee_ParseMessage($hash)) {
if (ParseMessage($hash)) {
delete $hash->{PARTIAL};
} else {
Log3 $hash->{NAME}, 2, "Connection :: Failed parsing buffer content: ".$hash->{PARTIAL};
@ -499,48 +568,24 @@ sub SmarterCoffee_Read($;$) {
return 1;
}
sub SmarterCoffee_Initialize($) {
my ($hash) = @_;
$hash->{DefFn} = 'SmarterCoffee_Define';
$hash->{UndefFn} = 'SmarterCoffee_Undefine';
$hash->{GetFn} = 'SmarterCoffee_Get';
$hash->{SetFn} = 'SmarterCoffee_Set';
$hash->{ReadFn} = 'SmarterCoffee_Read';
$hash->{ReadyFn} = 'SmarterCoffee_OpenIfRequiredAndWritePending';
$hash->{NotifyFn} = 'SmarterCoffee_Notify';
$hash->{AttrList} = ""
."default-hotplate-on-for-minutes "
."ignore-max-cups "
."set-on-brews-coffee "
."strength-coffee-weights "
."strength-extra-percent "
."strength-extra-pre-brew-cups "
."strength-extra-pre-brew-delay-seconds "
."strength-extra-start-on-device-strength:off,weak,medium,strong "
.$readingFnAttributes;
Log 5, "Initialized module 'SmarterCoffee'";
}
sub SmarterCoffee_Define($$) {
sub Define($$) {
my ($hash, $def) = @_;
my @param = split('[ \t]+', $def);
my $name = $hash->{NAME};
# set default settings on first define
if ($init_done) {
$attr{$name}{alias} = "Coffee Machine";
$attr{$name}{webCmd} = "strength:cups:start:hotplate:off";
$attr{$name}{'strength-extra-percent'} = $SmarterCoffee_StrengthExtraDefaultPercent;
$attr{$name}{'default-hotplate-on-for-minutes'} = "15 5=20 8=30 10=35";
$attr{$name}{'event-on-change-reading'} = ".*";
$attr{$name}{'event-on-update-reading'} = "last_command.*";
CommandAttr(undef,$name . ' alias Coffee Machine') if ( AttrVal($name,'alias','none') eq 'none' );
CommandAttr(undef,$name . ' webCmd strength:cups:start:hotplate:off') if ( AttrVal($name,'webCmd','none') eq 'none' );
CommandAttr(undef,$name . ' strength-extra-percent ' . $strengthExtraDefaultPercent) if ( AttrVal($name,'strength-extra-percent','none') eq 'none' );
CommandAttr(undef,$name . ' default-hotplate-on-for-minutes 15 5=20 8=30 10=35') if ( AttrVal($name,'default-hotplate-on-for-minutes','none') eq 'none' );
CommandAttr(undef,$name . ' event-on-change-reading .*') if ( AttrVal($name,'event-on-change-reading','none') eq 'none' );
CommandAttr(undef,$name . ' event-on-update-reading last_command.*') if ( AttrVal($name,'event-on-update-reading','none') eq 'none' );
}
$attr{$name}{devStateIcon} = '{ SmarterCoffee_GetDevStateIcon($name) }' if not defined($attr{$name}{devStateIcon});
CommandAttr(undef,$name . 'devStateIcon { SmarterCoffee::GetDevStateIcon($name) }') if ( AttrVal($name,'devStateIcon','none') eq 'none' or AttrVal($name,'devStateIcon','none') eq '{ SmarterCoffee_GetDevStateIcon($name) }' );
$hash->{VERSION} = $version;
if (int(@param) < 3) {
$hash->{AUTO_DETECT} = 1;
} else {
@ -552,38 +597,62 @@ sub SmarterCoffee_Define($$) {
readingsSingleUpdate($hash,'state','initializing',0);
$hash->{".last_command"} =
$hash->{".last_response"} =
$hash->{".last_status"} =
$hash->{".raw_last_status"} = "";
$hash->{".last_response"} =
$hash->{".last_status"} =
$hash->{".raw_last_status"} = "";
SmarterCoffee_Connect($hash);
Connect($hash);
$modules{SmarterCoffee}{defptr}{CoolTux} = $hash;
Log3 $hash->{NAME}, 4, "Instance :: Defined module 'SmarterCoffee': ".Dumper($hash);
}
sub SmarterCoffee_Undefine($$) {
sub Undefine($$) {
my ($hash, $arg) = @_;
RemoveInternalTimer($hash);
DevIo_CloseDev($hash);
main::DevIo_CloseDev($hash);
Log3 $hash->{NAME}, 4, "Instance :: Closed module 'SmarterCoffee': ".Dumper($hash);
delete( $modules{SmarterCoffee}{defptr}{CoolTux} );
return undef;
}
sub SmarterCoffee_Get {
sub Attr(@) {
my ( $cmd, $name, $attrName, $attrVal ) = @_;
my $hash = $defs{$name};
if( $attrName eq "devioLoglevel" ) {
if( $cmd eq "set" ) {
$hash->{devioLoglevel} = $attrVal;
Log3 $name, 3, "SmarterCoffee ($name) - set devioLoglevel to $attrVal";
} elsif( $cmd eq "del" ) {
delete $hash->{devioLoglevel};
Log3 $name, 3, "SmarterCoffee ($name) - delete Internal devioLoglevel";
}
}
return undef;
}
sub Get {
my ($hash, @param) = @_;
if (grep {$_ eq ($param[1] // "")} @SmarterCoffee_GetCommands) {
return SmarterCoffee_Set($hash, @param) // "Ok :: ".$hash->{".last_response"};
if (grep {$_ eq ($param[1] // "")} @getCommands) {
return Set($hash, @param) // "Ok :: ".$hash->{".last_response"};
} else {
return "Unknown argument $param[1], choose one of ".join(":noArg ", @SmarterCoffee_GetCommands).":noArg";
return "Unknown argument $param[1], choose one of ".join(":noArg ", @getCommands).":noArg";
}
}
sub SmarterCoffee_Set {
sub Set {
my ($hash, @param) = @_;
my $desiredCups = defined($hash->{".extra_strength.original_desired_cups"})
@ -599,7 +668,7 @@ sub SmarterCoffee_Set {
# Special treatment for hotplate, syntax: "set hotplate (on|off) [5-40]"
if ($option =~ /^hotplate.*/) {
# Select default time from "[minutes] [cups=minutes]", e.g.: "15 5=20 10=35" means: 15 default, 20 from 5 cups and 35 from 10 cups.
my ($defaultOnForMinutes, $overrides) = parseParams(AttrVal($hash->{NAME}, "default-hotplate-on-for-minutes", $SmarterCoffee_Hotplate{default}));
my ($defaultOnForMinutes, $overrides) = parseParams(AttrVal($hash->{NAME}, "default-hotplate-on-for-minutes", $hotplate{default}));
$defaultOnForMinutes = $defaultOnForMinutes->[0] if (defined($defaultOnForMinutes) and int($defaultOnForMinutes) > 0);
for my $key (sort { $a <=> $b } (keys %{$overrides})) {
$defaultOnForMinutes = $overrides->{$key} if (int($desiredCups) >= int($key));
@ -607,17 +676,17 @@ sub SmarterCoffee_Set {
$value = $optionValue if (not defined($value));
$value = $value =~ /^[0-9]+$/ ? int($value) : int($defaultOnForMinutes);
$value = $SmarterCoffee_Hotplate{max} if ($value > $SmarterCoffee_Hotplate{max});
$value = $SmarterCoffee_Hotplate{min} if ($value < $SmarterCoffee_Hotplate{min});
$value = $hotplate{max} if ($value > $hotplate{max});
$value = $hotplate{min} if ($value < $hotplate{min});
SmarterCoffee_UpdateReading($hash, "hotplate_on_for_minutes", ($option eq "hotplate_off" ? 0 : $value));
UpdateReading($hash, "hotplate_on_for_minutes", ($option eq "hotplate_off" ? 0 : $value));
return unpack('H*', pack('C', $value));
} elsif (defined($SmarterCoffee_MessageMaps{$option}) and defined($optionValue)) {
} elsif (defined($messageMaps{$option}) and defined($optionValue)) {
# Ordinary values are looked up in the message maps (looking up the HEX code that backs a setting).
for my $key (keys %{$SmarterCoffee_MessageMaps{$option}}) {
my $v = $SmarterCoffee_MessageMaps{$option}{$key};
for my $key (keys %{$messageMaps{$option}}) {
my $v = $messageMaps{$option}{$key};
if ((ref($v) eq "HASH" ? grep(/^$optionValue$/, values %{$v}) : $v eq $optionValue)) {
return $key;
}
@ -651,13 +720,13 @@ sub SmarterCoffee_Set {
# Enable grinder in extra mode if required and option is not defaults.
my $grinderEnabled = (($param[3] // ReadingsVal($hash->{NAME}, "grinder", "")) eq "enabled");
if ($option ne "defaults" and not $grinderEnabled and ($param[3] // "") ne "disabled") {
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "grinder", "enabled" ]});
Set($hash, @{[ $hash->{NAME}, "grinder", "enabled" ]});
$grinderEnabled = 1;
$param[3] = "enabled" if defined($param[3]);
}
if ($option ne "defaults" and $grinderEnabled) {
if (SmarterCoffee_TranslateParamsForExtraStrength($hash, \@param, "grind")) {
if (TranslateParamsForExtraStrength($hash, \@param, "grind")) {
my ($cups, $error) = ($hash->{".extra_strength.desired_cups"}, $hash->{".extra_strength.error_rate"});
Log3 $hash->{NAME}, 3, "Extra Strength :: Grinding [".join(" ", @param)."] to get $cups cups (error rate: $error%).";
} else {
@ -701,7 +770,7 @@ sub SmarterCoffee_Set {
$option = "brew_with_settings";
$messagePart = $input{cups}.$input{strength}.$input{hotplate}.$input{grinder};
SmarterCoffee_UpdateReadings($hash,
UpdateReadings($hash,
sub($) {
my ($updateReading) = @_;
for my $key (keys %readingsValues) { $updateReading->( $key, $readingsValues{$key} ) }
@ -727,7 +796,7 @@ sub SmarterCoffee_Set {
delete $hash->{".extra_strength.enabled"} if ($option eq "strength" and $param[0] ne "extra" and $hash->{".extra_strength.enabled"});
# Eager updating strength, cups and grinder reading to avoid that widget updates are slower than starting a "brew".
SmarterCoffee_UpdateReading($hash, $option, $param[0]) if ($option =~ /^(strength|cups|grinder)$/);
UpdateReading($hash, $option, $param[0]) if ($option =~ /^(strength|cups|grinder)$/);
# Aborting device update when strength is "extra".
return undef if ($option eq "strength" and $param[0] eq "extra");
@ -738,15 +807,15 @@ sub SmarterCoffee_Set {
# Resetting internal states before executing "stop".
if ($option eq "stop" and ($param[0] // "") ne "no-reset") {
SmarterCoffee_ResetState($hash);
ResetState($hash);
}
$messagePart = $optionToMessage->( $option, $param[0] );
}
# Command execution
if (defined($SmarterCoffee_Commands{$option})) {
my $message = $SmarterCoffee_Commands{$option};
if (defined($commands{$option})) {
my $message = $commands{$option};
# Replacing placeholders with value.
$message =~ s/#+/$messagePart/ if (defined($messagePart) and $messagePart =~ /^[a-f0-9]{2,}$/);
@ -758,14 +827,14 @@ sub SmarterCoffee_Set {
$hash->{"PENDING_COMMAND"} = $hash->{".last_command"} = $message;
Log3 $hash->{NAME}, 4, "Connection :: Sending message: $message [".$hash->{".last_set_command"}."]";
SmarterCoffee_WritePending($hash, ($option eq "info"));
WritePending($hash, ($option eq "info"));
}
return undef;
} elsif ($option eq "disconnect" or $option eq "reconnect") {
# This option is primarily to test if reconnect works.
DevIo_Disconnected($hash);
SmarterCoffee_Connect($hash) if ($option eq "reconnect");
main::DevIo_Disconnected($hash);
Connect($hash) if ($option eq "reconnect");
return undef;
} elsif ($option ne "?" and $option ne "help") {
@ -773,7 +842,7 @@ sub SmarterCoffee_Set {
}
my @strength = split(",", "weak,medium,strong,extra");
pop(@strength) if (not SmarterCoffee_IsExtraStrengthModeAvailable($hash));
pop(@strength) if (not IsExtraStrengthModeAvailable($hash));
return "Unknown argument $option, choose one of"
." brew"
@ -787,14 +856,14 @@ sub SmarterCoffee_Set {
." hotplate_on_for_minutes:slider,5,5,40";
}
sub SmarterCoffee_ResetState($) {
sub ResetState($) {
my ($hash) = @_;
SmarterCoffee_ResetBrewState($hash);
SmarterCoffee_ResetExtraStrengthMode($hash);
ResetBrewState($hash);
ResetExtraStrengthMode($hash);
}
sub SmarterCoffee_Notify($$) {
sub Notify($$) {
my ($hash, $eventHash) = @_;
my $name = $hash->{NAME};
my $senderName = $eventHash->{NAME};
@ -804,27 +873,27 @@ sub SmarterCoffee_Notify($$) {
if (my $events = deviceEvents($eventHash, 1)) {
if ($senderName eq "global") {
SmarterCoffee_ReadConfiguration($hash) if (grep(m/^(INITIALIZED|REREADCFG)$/, @{$events}));
ReadConfiguration($hash) if (grep(m/^(INITIALIZED|REREADCFG)$/, @{$events}));
} else {
for (@{$events}) {
if ($_) {
SmarterCoffee_ProcessBrewStateEvents($hash, $_);
SmarterCoffee_ProcessEventForExtraStrength($hash, $_);
SmarterCoffee_LogCommands($hash, $_);
ProcessBrewStateEvents($hash, $_);
ProcessEventForExtraStrength($hash, $_);
LogCommands($hash, $_);
}
}
}
}
}
sub SmarterCoffee_ReadConfiguration($$) {
sub ReadConfiguration($) {
my ($hash) = @_;
# Restoring extra strength
$hash->{".extra_strength.enabled"} = 1 if (ReadingsVal($hash->{NAME}, "strength", "") =~ /^extra.*/);
}
sub SmarterCoffee_LogCommands($$) {
sub LogCommands($$) {
my ($hash, $event) = @_;
if ($event =~ /^last_command_success:\s*(yes|no)\s*$/i and (my $command = ReadingsVal($hash->{NAME}, "last_command", 0))) {
@ -837,7 +906,7 @@ sub SmarterCoffee_LogCommands($$) {
}
}
sub SmarterCoffee_ProcessBrewStateEvents($$) {
sub ProcessBrewStateEvents($$) {
my ($hash, $event) = @_;
# Setting "INITIATED_BREWING" when brewing was initiated by a command (and not by using the machine's buttons)
@ -849,26 +918,26 @@ sub SmarterCoffee_ProcessBrewStateEvents($$) {
$hash->{".brew-state"} = $1;
} elsif ($event =~ /^state:\s*done/) {
SmarterCoffee_ResetBrewState($hash);
ResetBrewState($hash);
} elsif ($event =~ /^state:\s*(.+)$/ and ($hash->{".brew-state"} // "") =~ /^(brewing|grinding)$/) {
Log3 $hash->{NAME}, 3, "Found state change from 'brewing' to '$1'. This looks like an abort, resetting all states to initial.";
SmarterCoffee_ResetState($hash);
ResetState($hash);
}
}
sub SmarterCoffee_ResetBrewState($) {
sub ResetBrewState($) {
my ($hash) = @_;
delete $hash->{".brew-state"} if defined($hash->{".brew-state"});
delete $hash->{"INITIATED_BREWING"} if defined($hash->{"INITIATED_BREWING"});
}
sub SmarterCoffee_ProcessEventForExtraStrength($$) {
sub ProcessEventForExtraStrength($$) {
my ($hash, $event) = @_;
if ($event =~ /^strength:\s*extra\s*$/) {
# Listen to "set strength extra" and enable it if available.
if (not (SmarterCoffee_EnableExtraStrengthMode($hash))) {
if (not (EnableExtraStrengthMode($hash))) {
Log3 $hash->{NAME}, 3, "Extra-Strength :: Downgrading strength 'extra' to 'strong'";
fhem("sleep 0.1 fix-strength ; set ".$hash->{NAME}." strength strong");
}
@ -878,11 +947,11 @@ sub SmarterCoffee_ProcessEventForExtraStrength($$) {
if (ReadingsVal($hash->{NAME}, "grinder", "-") eq "disabled"
and (my $cups = int(ReadingsVal($hash->{NAME}, "cups", 0))) > 0
and (my $strength = ReadingsVal($hash->{NAME}, "strength", "")) eq AttrVal($hash->{NAME}, "strength-extra-start-on-device-strength", "off")
and SmarterCoffee_EnableExtraStrengthMode($hash) ) {
and EnableExtraStrengthMode($hash) ) {
Log3 $hash->{NAME}, 3, "Extra-Strength :: Upgrading brewing $cups cups started with disabled grinder and strength '$strength' to strength 'extra'.";
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "stop" ]});
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "brew", $cups, "extra" ]});
Set($hash, @{[ $hash->{NAME}, "stop" ]});
Set($hash, @{[ $hash->{NAME}, "brew", $cups, "extra" ]});
}
} elsif (($hash->{".extra_strength.enabled"} or $hash->{".extra_strength.phase-2"})) {
@ -893,22 +962,22 @@ sub SmarterCoffee_ProcessEventForExtraStrength($$) {
} elsif ($event =~ /^state:\s*done/) {
# Finishing first round (grinding & first brew are done here)
if ((my $delay = int($hash->{".extra_strength.pre_brew_phase_delay"} // 0)) > 0) {
InternalTimer(gettimeofday() + $delay, "SmarterCoffee_ExtraStrengthHandleBrewing", $hash, 0);
InternalTimer(gettimeofday() + $delay, "SmarterCoffee::ExtraStrengthHandleBrewing", $hash, 0);
} else {
if (int($hash->{".extra_strength.original_desired_cups"} // 0) > 0) {
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "cups", $hash->{".extra_strength.original_desired_cups"} ]});
Set($hash, @{[ $hash->{NAME}, "cups", $hash->{".extra_strength.original_desired_cups"} ]});
}
SmarterCoffee_ResetExtraStrengthMode($hash);
ResetExtraStrengthMode($hash);
}
} elsif ($event =~ /^state:\s*brewing/ and not $hash->{".extra_strength.phase-2"}) {
# Entering phase-2: Brewing after initial grinding at different settings.
$hash->{".extra_strength.phase-2"} = SmarterCoffee_ExtraStrengthHandleBrewing($hash);
$hash->{".extra_strength.phase-2"} = ExtraStrengthHandleBrewing($hash);
}
}
}
sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
sub ExtraStrengthHandleBrewing($) {
my ($hash) = @_;
my @params = (
ReadingsVal($hash->{NAME}, "cups", "-"),
@ -917,12 +986,12 @@ sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
"disabled"
);
if (SmarterCoffee_TranslateParamsForExtraStrength($hash, \@params, "brew")) {
if (TranslateParamsForExtraStrength($hash, \@params, "brew")) {
# Resetting brew state to ensure it doesn't interfere with stop command that runs with "no-reset" option.
SmarterCoffee_ResetBrewState($hash);
ResetBrewState($hash);
# Stopping brewing after initial grinding (skip stop if we are in phase-2 and came here due to pre-brew delay)
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "stop", "no-reset" ]}) if not $hash->{".extra_strength.phase-2"};
Set($hash, @{[ $hash->{NAME}, "stop", "no-reset" ]}) if not $hash->{".extra_strength.phase-2"};
unshift(@params, "brew");
unshift(@params, $hash->{NAME});
@ -932,17 +1001,17 @@ sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
: "2";
Log3 $hash->{NAME}, 4, "Extra-Strength :: Phase $phase [set ".join(" ", @params)."]";
SmarterCoffee_Set($hash, @params);
Set($hash, @params);
return 1;
}
return 0;
}
sub SmarterCoffee_IsExtraStrengthModeAvailable($;$) {
sub IsExtraStrengthModeAvailable($;$) {
my ($hash, $slient) = @_;
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $SmarterCoffee_StrengthExtraDefaultPercent);
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $strengthExtraDefaultPercent);
my $preBrew = int(AttrVal($hash->{NAME}, "strength-extra-pre-brew-cups", 1)) * int(AttrVal($hash->{NAME}, "strength-extra-pre-brew-delay-seconds", 0));
if ($extraPercent > 0 and ($extraPercent != 1 or $preBrew > 0) and $extraPercent < 2.5) {
@ -954,12 +1023,12 @@ sub SmarterCoffee_IsExtraStrengthModeAvailable($;$) {
}
}
sub SmarterCoffee_EnableExtraStrengthMode($) {
sub EnableExtraStrengthMode($) {
my ($hash) = @_;
return 1 if ($hash->{".extra_strength.enabled"});
if (SmarterCoffee_IsExtraStrengthModeAvailable($hash, 0)) {
if (IsExtraStrengthModeAvailable($hash, 0)) {
Log3 $hash->{NAME}, 4, "Extra-Strength :: Entering extra strength mode.";
$hash->{".extra_strength.enabled"} = 1;
return 1;
@ -968,7 +1037,7 @@ sub SmarterCoffee_EnableExtraStrengthMode($) {
}
}
sub SmarterCoffee_ResetExtraStrengthMode($;$) {
sub ResetExtraStrengthMode($;$) {
my ($hash, $partial) = @_;
Log3 $hash->{NAME}, 4, ("Extra-Strength :: Resetting state to initial (partial: " . ($partial // 0) . ").");
@ -986,16 +1055,16 @@ sub SmarterCoffee_ResetExtraStrengthMode($;$) {
}
}
sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
sub TranslateParamsForExtraStrength($$$) {
my ($hash, $params, $phase) = @_;
return 0 if (not (SmarterCoffee_EnableExtraStrengthMode($hash)));
return 0 if (not (EnableExtraStrengthMode($hash)));
if ($phase eq "grind") {
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $SmarterCoffee_StrengthExtraDefaultPercent);
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $strengthExtraDefaultPercent);
my @strengths = ("weak", "medium", "strong");
my @weights = split(/\s+/, AttrVal($hash->{NAME}, "strength-coffee-weights", $SmarterCoffee_StrengthDefaultWeights));
my @weights = split(/\s+/, AttrVal($hash->{NAME}, "strength-coffee-weights", $strengthDefaultWeights));
while (int(@weights) < 3) { push(@weights, (int(@weights) ? $weights[int(@weights) - 1] : 4.3)) }
Log3 $hash->{NAME}, 4, "Extra-Strength :: Reference weights: ".join(" ", @weights)." (".join(" ", @strengths).")";
@ -1059,7 +1128,7 @@ sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
$params->[0] = $preBrewCups;
} else {
$params->[0] = $hash->{".extra_strength.desired_cups"};
SmarterCoffee_ResetExtraStrengthMode($hash, 1);
ResetExtraStrengthMode($hash, 1);
}
return 1;
@ -1068,12 +1137,12 @@ sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
return 0;
}
sub SmarterCoffee_UpdateReading($$$) {
sub UpdateReading($$$) {
my ($hash, $name, $value) = @_;
SmarterCoffee_UpdateReadings($hash, sub($) { ($_[0])->( $name, $value ) });
UpdateReadings($hash, sub($) { ($_[0])->( $name, $value ) });
}
sub SmarterCoffee_UpdateReadings($$;$) {
sub UpdateReadings($$;$) {
my ($hash, $callback, $forceUpdate) = @_;
$forceUpdate = (($forceUpdate // 0)
@ -1099,29 +1168,29 @@ sub SmarterCoffee_UpdateReadings($$;$) {
readingsEndUpdate($hash, ($updated or $forceUpdate));
}
sub SmarterCoffee_RunDiscoveryProcess($;$) {
sub RunDiscoveryProcess($;$) {
my ($hash, $skipConnect) = @_;
if (SmarterCoffee_Discover($hash) and not $skipConnect) {
SmarterCoffee_Connect($hash);
if (Discover($hash) and not $skipConnect) {
Connect($hash);
}
InternalTimer(gettimeofday() + $SmarterCoffee_DiscoveryInterval, "SmarterCoffee_RunDiscoveryProcess", $hash, 0);
InternalTimer(gettimeofday() + $discoveryInterval, "SmarterCoffee::RunDiscoveryProcess", $hash, 0);
}
sub SmarterCoffee_InetSocketAddressString($) {
my ($port, $inetAddress) = sockaddr_in($_[0]);
return inet_ntoa($inetAddress).":$port"
sub InetSocketAddressString($) {
my ($sport, $inetAddress) = sockaddr_in($_[0]);
return inet_ntoa($inetAddress).":$sport"
}
sub SmarterCoffee_Discover($) {
sub Discover($) {
my ($hash) = @_;
my $existingDeviceName = ($hash->{DeviceName} // "");
my $broadcastAddress = sockaddr_in($SmarterCoffee_Port, INADDR_BROADCAST);
my $broadcastAddress = sockaddr_in($port, INADDR_BROADCAST);
Log3 $hash->{NAME}, 4,
"Discovery :: Broadcasting discovery request to ".SmarterCoffee_InetSocketAddressString($broadcastAddress)." (already discovered: $existingDeviceName)";
"Discovery :: Broadcasting discovery request to ".InetSocketAddressString($broadcastAddress)." (already discovered: $existingDeviceName)";
socket(my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp'));
setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1);
@ -1130,17 +1199,17 @@ sub SmarterCoffee_Discover($) {
while ($wait->can_read( 10 )) {
my $deviceAddress = recv($socket, my $message, 128, 0);
my $inetSocketAddress = SmarterCoffee_InetSocketAddressString($deviceAddress);
my $inetSocketAddress = InetSocketAddressString($deviceAddress);
$message = unpack('H*', $message);
Log3 $hash->{NAME}, 4, "Discovery :: Received message $message from $inetSocketAddress";
if ($message =~ /^65.*7e.*/ and SmarterCoffee_ParseMessage($hash, $message)) {
my ($port, $inetAddress) = sockaddr_in($deviceAddress);
if ($message =~ /^65.*7e.*/ and ParseMessage($hash, $message)) {
my ($sport, $inetAddress) = sockaddr_in($deviceAddress);
if (my ($hostname) = gethostbyaddr($inetAddress, AF_INET)) {
$hash->{DeviceName} = $hostname.":$port";
$hash->{DeviceName} = $hostname.":$sport";
} else {
$hash->{DeviceName} = $inetSocketAddress;
}
@ -1247,7 +1316,7 @@ my $SmarterCoffee_StatusIconSVG = <<XML;
</svg>
XML
sub SmarterCoffee_GetDevStateIcon {
sub GetDevStateIcon {
my ($name, $colors) = @_;
my ($state, $icon) = (Value($name), $SmarterCoffee_StatusIconSVG);
@ -1467,14 +1536,14 @@ sub SmarterCoffee_GetDevStateIcon {
<b>Attributes</b><br>
<ul>
<li>
<code>attr &lt;name&gt; devStateIcon { SmarterCoffee_GetDevStateIcon($name) }</code>
<code>attr &lt;name&gt; devStateIcon { SmarterCoffee::GetDevStateIcon($name) }</code>
<br><br>
The function <code>SmarterCoffee_GetDevStateIcon($name[, "...colors..."])</code> renders a custom dev state icon that displays
The function <code>SmarterCoffee::GetDevStateIcon($name[, "...colors..."])</code> renders a custom dev state icon that displays
the machine states (ready, brewing, done) and shows information on carafe, hotplate and water level.
<br><br>
The icon is monochrome using a default color that may change to highlight states: ready, brewing, done.
Built-in colors can be adjusted with the second parameter of <code>SmarterCoffee_GetDevStateIcon</code>.<br>
E.g. using "<code>attr &lt;name&gt; devStateIcon { SmarterCoffee_GetDevStateIcon($name, '#7b7b7b green chocolate #336699' }</code>"
Built-in colors can be adjusted with the second parameter of <code>SmarterCoffee::GetDevStateIcon</code>.<br>
E.g. using "<code>attr &lt;name&gt; devStateIcon { SmarterCoffee::GetDevStateIcon($name, '#7b7b7b green chocolate #336699' }</code>"
sets colors for default, ready, brewing and done.
<br><br>
Colors are specified as HTML color values delimited by whitespace using a fixed order of "default ready brewing done".