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 # Copyright notice
# #
# (c) 2016 # (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 # All rights reserved
# #
# This script free software; you can redistribute it and/or modify # This script free software; you can redistribute it and/or modify
@ -38,10 +39,13 @@
# - https://github.com/Tristan79/iBrew # - https://github.com/Tristan79/iBrew
# .. and to all the volonteers crafting the FHEM project. # .. 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 # v0.9.1 - 2017-04-25
# - fixed "stop" detection interferring with "extra" strength. # - fixed "stop" detection interferring with "extra" strength.
# - added new state "grinding". # - added new state "grinding".
@ -106,20 +110,86 @@ package main;
use strict; use strict;
use warnings; 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 Data::Dumper;
use Socket; use Socket;
use IO::Select; use IO::Select;
use DevIo; use DevIo;
use HttpUtils; #use HttpUtils;
my $SmarterCoffee_Port = 2081; ## Import der FHEM Funktionen
my $SmarterCoffee_DiscoveryInterval = 60 * 15; BEGIN {
my $SmarterCoffee_StrengthExtraDefaultPercent = 1.4; GP_Import(
my $SmarterCoffee_StrengthDefaultWeights = "3.5 3.9 4.3"; qw(readingsSingleUpdate
my %SmarterCoffee_Hotplate = (default => 15, min => 5, max => 40); 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 => [ status_bitmasks => [
# BIT 1 = ??? # BIT 1 = ???
# BIT 2 = hotplate # BIT 2 = hotplate
@ -167,7 +237,7 @@ my %SmarterCoffee_MessageMaps = (
} }
); );
my %SmarterCoffee_Commands = ( my %commands = (
reset => "107e", reset => "107e",
brew => "377e", brew => "377e",
brew_with_settings => "33########7e", brew_with_settings => "33########7e",
@ -185,9 +255,9 @@ my %SmarterCoffee_Commands = (
history => "467e" 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' }, '00' => { message => 'Ok', success => 'yes' },
'01' => { message => 'Ok, brewing in progress', 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) = @_; my ($hash, $message) = @_;
$message = ($hash->{PARTIAL} // "") if (not defined($message)); $message = ($hash->{PARTIAL} // "") if (not defined($message));
@ -214,7 +284,7 @@ sub SmarterCoffee_ParseMessage {
if (int(@messages) > 1) { if (int(@messages) > 1) {
my $failed; my $failed;
for (@messages) { for (@messages) {
$failed = 1 if (not SmarterCoffee_ParseMessage($hash, $_."7e")); $failed = 1 if (not ParseMessage($hash, $_."7e"));
} }
return not $failed; return not $failed;
} }
@ -224,16 +294,15 @@ sub SmarterCoffee_ParseMessage {
# Parse response of a command. # Parse response of a command.
if ($message =~ /^03([0-9a-f]{2})7e.*/) { if ($message =~ /^03([0-9a-f]{2})7e.*/) {
if (my $response = ($SmarterCoffee_ResponseCodes{$1} // 0)) { if (my $response = ($responseCodes{$1} // 0)) {
SmarterCoffee_UpdateReadings($hash, UpdateReadings($hash,
sub($) { sub($) {
my ($updateReading) = @_; my ($updateReading) = @_;
while (my ($key, $value) = each %{$response}) { while (my ($key, $value) = each %{$response}) {
$updateReading->( "last_command_$key", $value ); $updateReading->( "last_command_$key", $value );
} }
$updateReading->( "last_command", $hash->{".last_set_command"} ); $updateReading->( "last_command", $hash->{".last_set_command"} );
}, 1 }, 1);
, 1);
} else { } else {
Log3 $hash->{NAME}, 3, "Connection :: Unknown command response '$message'."; Log3 $hash->{NAME}, 3, "Connection :: Unknown command response '$message'.";
} }
@ -243,7 +312,7 @@ sub SmarterCoffee_ParseMessage {
if ($message =~ /^47([0-9a-f]{2})(.+)7e.*/) { if ($message =~ /^47([0-9a-f]{2})(.+)7e.*/) {
my @history = split("7d", $2); my @history = split("7d", $2);
Log 2, Dumper(@history); #TODO Log3 $hash->{NAME}, 5, Dumper(@history); #TODO
} }
# Parse default settings message. # Parse default settings message.
@ -255,19 +324,19 @@ sub SmarterCoffee_ParseMessage {
hotplate => substr($1, 6, 2), hotplate => substr($1, 6, 2),
); );
SmarterCoffee_ParseStatusValues($hash, \%values); ParseStatusValues($hash, \%values);
DoTrigger($hash->{NAME}, "get_defaults"); DoTrigger($hash->{NAME}, "get_defaults");
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "defaults" ]}); Set($hash, @{[ $hash->{NAME}, "defaults" ]});
} }
# Parse carafe detection status message. # Parse carafe detection status message.
if ($message =~ /^4d([0-9a-f]{2})7e.*/) { 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. # Parse single cup mode status message.
if ($message =~ /^50([0-9a-f]{2})7e.*/) { 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. # Parse info & discovery message.
@ -287,7 +356,7 @@ sub SmarterCoffee_ParseMessage {
cups => '0'.substr($message, 11, 1), cups => '0'.substr($message, 11, 1),
); );
SmarterCoffee_ParseStatusValues($hash, \%values); ParseStatusValues($hash, \%values);
} }
$hash->{CONNECTION} = "" $hash->{CONNECTION} = ""
@ -301,14 +370,14 @@ sub SmarterCoffee_ParseMessage {
return 0; return 0;
} }
sub SmarterCoffee_DumpToExpression($) { sub DumpToExpression($) {
my $d = Dumper($_[0]); my $d = Dumper($_[0]);
$d =~ s/\s+/ /g; $d =~ s/\s+/ /g;
$d =~ s/[^\}]*(\{.+\})[^\}]*/$1/; $d =~ s/[^\}]*(\{.+\})[^\}]*/$1/;
return $d; return $d;
} }
sub SmarterCoffee_ParseStatusValues { sub ParseStatusValues {
my ($hash, $values) = @_; my ($hash, $values) = @_;
while (my ($mappingKey, $rawValue) = each %{$values}) { while (my ($mappingKey, $rawValue) = each %{$values}) {
@ -317,20 +386,20 @@ sub SmarterCoffee_ParseStatusValues {
my $unpackedStatusBits = sprintf('%08b', ord(pack("H2", $rawValue))); my $unpackedStatusBits = sprintf('%08b', ord(pack("H2", $rawValue)));
$hash->{".last_status"} .= " ($unpackedStatusBits)"; $hash->{".last_status"} .= " ($unpackedStatusBits)";
for (@{$SmarterCoffee_MessageMaps{"status_bitmasks"}}) { for (@{$messageMaps{"status_bitmasks"}}) {
my ($unpackedBitmask, $statusInfo) = @{$_}; my ($unpackedBitmask, $statusInfo) = @{$_};
my $bitmask = ord(pack("B8", $unpackedBitmask)); my $bitmask = ord(pack("B8", $unpackedBitmask));
if (($bitmask & ord(pack("B8", $unpackedStatusBits))) == $bitmask) { if (($bitmask & ord(pack("B8", $unpackedStatusBits))) == $bitmask) {
while (my ($k, $v) = each(%{$statusInfo})) { $status{$k} = $v } 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 }; $values->{$mappingKey} = { %status };
} else { } else {
if (defined($SmarterCoffee_MessageMaps{$mappingKey}{$rawValue})) { if (defined($messageMaps{$mappingKey}{$rawValue})) {
$values->{$mappingKey} = $SmarterCoffee_MessageMaps{$mappingKey}{$rawValue}; $values->{$mappingKey} = $messageMaps{$mappingKey}{$rawValue};
} elsif ($mappingKey eq "hotplate") { } elsif ($mappingKey eq "hotplate") {
$values->{$mappingKey} = { "hotplate_on_for_minutes" => hex($rawValue) }; $values->{$mappingKey} = { "hotplate_on_for_minutes" => hex($rawValue) };
} else { } else {
@ -342,7 +411,7 @@ sub SmarterCoffee_ParseStatusValues {
Log3 $hash->{NAME}, 5, "Connection :: Parsed message: ".Dumper($values); Log3 $hash->{NAME}, 5, "Connection :: Parsed message: ".Dumper($values);
SmarterCoffee_UpdateReadings($hash, UpdateReadings($hash,
sub($) { sub($) {
my ($updateReading) = @_; my ($updateReading) = @_;
my $state = 0; my $state = 0;
@ -384,7 +453,7 @@ sub SmarterCoffee_ParseStatusValues {
); );
} }
sub SmarterCoffee_Connect($) { sub Connect($) {
my ($hash) = @_; my ($hash) = @_;
my $isNewConnection = ReadingsVal($hash->{NAME},'state','none') eq "initializing"; 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}); delete $hash->{INVALID_DEVICE} if defined($hash->{INVALID_DEVICE});
if ($hash->{AUTO_DETECT}) { if ($hash->{AUTO_DETECT}) {
SmarterCoffee_RunDiscoveryProcess($hash, 1); RunDiscoveryProcess($hash, 1);
} }
if (defined($hash->{DeviceName})) { if (defined($hash->{DeviceName})) {
if (not ($hash->{DeviceName} =~ m/^(.+):([0-9]+)$/)) { 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}); delete $hash->{DevIoJustClosed} if ($hash->{DevIoJustClosed});
return SmarterCoffee_OpenIfRequiredAndWritePending($hash, $isNewConnection); return OpenIfRequiredAndWritePending($hash, $isNewConnection);
} }
return 0; return 0;
} }
sub SmarterCoffee_OpenIfRequiredAndWritePending($;$) { sub OpenIfRequiredAndWritePending($;$) {
my ($hash, $initial) = @_; 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) = @_; my ($hash) = @_;
return if ($hash->{".initial-connection-state"}); 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; $hash->{".initial-connection-state"} = 1;
readingsSingleUpdate($hash,'state','connected',0); readingsSingleUpdate($hash,'state','connected',0);
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "info" ]}) if (not $hash->{AUTO_DETECT}); Get($hash, @{[ $hash->{NAME}, "info" ]}) if (not $hash->{AUTO_DETECT});
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "carafe_required_status" ]}); Get($hash, @{[ $hash->{NAME}, "carafe_required_status" ]});
SmarterCoffee_Get($hash, @{[ $hash->{NAME}, "cups_single_mode_status" ]}); Get($hash, @{[ $hash->{NAME}, "cups_single_mode_status" ]});
delete $hash->{".initial-connection-state"}; delete $hash->{".initial-connection-state"};
} }
} }
sub SmarterCoffee_WritePending { sub WritePending {
my ($hash, $mustSucceed) = @_; my ($hash, $mustSucceed) = @_;
if (DevIo_IsOpen($hash)) { if (main::DevIo_IsOpen($hash)) {
my $pending = ($hash->{PENDING_COMMAND} // 0); my $pending = ($hash->{PENDING_COMMAND} // 0);
# Handling initial call on a fresh connection # Handling initial call on a fresh connection
SmarterCoffee_HandleInitialConnectState($hash); HandleInitialConnectState($hash);
# Processing pending commands # Processing pending commands
if (($hash->{INVALID_DEVICE} // "0") eq "1") { if (($hash->{INVALID_DEVICE} // "0") eq "1") {
@ -448,14 +517,14 @@ sub SmarterCoffee_WritePending {
delete $hash->{PENDING_COMMAND} if defined($hash->{PENDING_COMMAND}); delete $hash->{PENDING_COMMAND} if defined($hash->{PENDING_COMMAND});
Log3 $hash->{NAME}, 4, "Connection :: Sending to ".$hash->{DeviceName}.": $pending"; 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"} = ""; $hash->{".raw_last_status"} = "";
my $result = DevIo_SimpleReadWithTimeout($hash, 5); my $result = main::DevIo_SimpleReadWithTimeout($hash, 5);
if ($result) { if ($result) {
$result = SmarterCoffee_Read($hash, $result); $result = Read($hash, $result);
} else { } else {
DevIo_Disconnected($hash); main::DevIo_Disconnected($hash);
} }
$hash->{INVALID_DEVICE} = "1" if ($mustSucceed and not $result); $hash->{INVALID_DEVICE} = "1" if ($mustSucceed and not $result);
@ -467,11 +536,11 @@ sub SmarterCoffee_WritePending {
return undef; return undef;
} }
sub SmarterCoffee_Read($;$) { sub Read($;$) {
my ($hash, $buffer) = @_; my ($hash, $buffer) = @_;
# Handle case that fhem reconnected a broken connection and state is "opened". # 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. # Abort read if we already detected that the device is invalid.
return 0 if ($hash->{INVALID_DEVICE} // 0); 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); $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). # 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)); return 0 if (not defined($buffer));
# Appending message bytes as hex string. # Appending message bytes as hex string.
@ -488,7 +557,7 @@ sub SmarterCoffee_Read($;$) {
# Parsing the message and populate readings. # Parsing the message and populate readings.
if ($hash->{PARTIAL} ne "") { if ($hash->{PARTIAL} ne "") {
if (SmarterCoffee_ParseMessage($hash)) { if (ParseMessage($hash)) {
delete $hash->{PARTIAL}; delete $hash->{PARTIAL};
} else { } else {
Log3 $hash->{NAME}, 2, "Connection :: Failed parsing buffer content: ".$hash->{PARTIAL}; Log3 $hash->{NAME}, 2, "Connection :: Failed parsing buffer content: ".$hash->{PARTIAL};
@ -499,48 +568,24 @@ sub SmarterCoffee_Read($;$) {
return 1; return 1;
} }
sub SmarterCoffee_Initialize($) { sub Define($$) {
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($$) {
my ($hash, $def) = @_; my ($hash, $def) = @_;
my @param = split('[ \t]+', $def); my @param = split('[ \t]+', $def);
my $name = $hash->{NAME}; my $name = $hash->{NAME};
# set default settings on first define # set default settings on first define
if ($init_done) { if ($init_done) {
$attr{$name}{alias} = "Coffee Machine"; CommandAttr(undef,$name . ' alias Coffee Machine') if ( AttrVal($name,'alias','none') eq 'none' );
$attr{$name}{webCmd} = "strength:cups:start:hotplate:off"; CommandAttr(undef,$name . ' webCmd strength:cups:start:hotplate:off') if ( AttrVal($name,'webCmd','none') eq 'none' );
$attr{$name}{'strength-extra-percent'} = $SmarterCoffee_StrengthExtraDefaultPercent; CommandAttr(undef,$name . ' strength-extra-percent ' . $strengthExtraDefaultPercent) if ( AttrVal($name,'strength-extra-percent','none') eq 'none' );
$attr{$name}{'default-hotplate-on-for-minutes'} = "15 5=20 8=30 10=35"; 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' );
$attr{$name}{'event-on-change-reading'} = ".*"; CommandAttr(undef,$name . ' event-on-change-reading .*') if ( AttrVal($name,'event-on-change-reading','none') eq 'none' );
$attr{$name}{'event-on-update-reading'} = "last_command.*"; 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) { if (int(@param) < 3) {
$hash->{AUTO_DETECT} = 1; $hash->{AUTO_DETECT} = 1;
} else { } else {
@ -552,38 +597,62 @@ sub SmarterCoffee_Define($$) {
readingsSingleUpdate($hash,'state','initializing',0); readingsSingleUpdate($hash,'state','initializing',0);
$hash->{".last_command"} = $hash->{".last_command"} =
$hash->{".last_response"} = $hash->{".last_response"} =
$hash->{".last_status"} = $hash->{".last_status"} =
$hash->{".raw_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); Log3 $hash->{NAME}, 4, "Instance :: Defined module 'SmarterCoffee': ".Dumper($hash);
} }
sub SmarterCoffee_Undefine($$) { sub Undefine($$) {
my ($hash, $arg) = @_; my ($hash, $arg) = @_;
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
DevIo_CloseDev($hash); main::DevIo_CloseDev($hash);
Log3 $hash->{NAME}, 4, "Instance :: Closed module 'SmarterCoffee': ".Dumper($hash); Log3 $hash->{NAME}, 4, "Instance :: Closed module 'SmarterCoffee': ".Dumper($hash);
delete( $modules{SmarterCoffee}{defptr}{CoolTux} );
return undef; 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) = @_; my ($hash, @param) = @_;
if (grep {$_ eq ($param[1] // "")} @SmarterCoffee_GetCommands) { if (grep {$_ eq ($param[1] // "")} @getCommands) {
return SmarterCoffee_Set($hash, @param) // "Ok :: ".$hash->{".last_response"}; return Set($hash, @param) // "Ok :: ".$hash->{".last_response"};
} else { } 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 ($hash, @param) = @_;
my $desiredCups = defined($hash->{".extra_strength.original_desired_cups"}) 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]" # Special treatment for hotplate, syntax: "set hotplate (on|off) [5-40]"
if ($option =~ /^hotplate.*/) { 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. # 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); $defaultOnForMinutes = $defaultOnForMinutes->[0] if (defined($defaultOnForMinutes) and int($defaultOnForMinutes) > 0);
for my $key (sort { $a <=> $b } (keys %{$overrides})) { for my $key (sort { $a <=> $b } (keys %{$overrides})) {
$defaultOnForMinutes = $overrides->{$key} if (int($desiredCups) >= int($key)); $defaultOnForMinutes = $overrides->{$key} if (int($desiredCups) >= int($key));
@ -607,17 +676,17 @@ sub SmarterCoffee_Set {
$value = $optionValue if (not defined($value)); $value = $optionValue if (not defined($value));
$value = $value =~ /^[0-9]+$/ ? int($value) : int($defaultOnForMinutes); $value = $value =~ /^[0-9]+$/ ? int($value) : int($defaultOnForMinutes);
$value = $SmarterCoffee_Hotplate{max} if ($value > $SmarterCoffee_Hotplate{max}); $value = $hotplate{max} if ($value > $hotplate{max});
$value = $SmarterCoffee_Hotplate{min} if ($value < $SmarterCoffee_Hotplate{min}); $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)); 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). # 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}}) { for my $key (keys %{$messageMaps{$option}}) {
my $v = $SmarterCoffee_MessageMaps{$option}{$key}; my $v = $messageMaps{$option}{$key};
if ((ref($v) eq "HASH" ? grep(/^$optionValue$/, values %{$v}) : $v eq $optionValue)) { if ((ref($v) eq "HASH" ? grep(/^$optionValue$/, values %{$v}) : $v eq $optionValue)) {
return $key; return $key;
} }
@ -651,13 +720,13 @@ sub SmarterCoffee_Set {
# Enable grinder in extra mode if required and option is not defaults. # Enable grinder in extra mode if required and option is not defaults.
my $grinderEnabled = (($param[3] // ReadingsVal($hash->{NAME}, "grinder", "")) eq "enabled"); my $grinderEnabled = (($param[3] // ReadingsVal($hash->{NAME}, "grinder", "")) eq "enabled");
if ($option ne "defaults" and not $grinderEnabled and ($param[3] // "") ne "disabled") { 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; $grinderEnabled = 1;
$param[3] = "enabled" if defined($param[3]); $param[3] = "enabled" if defined($param[3]);
} }
if ($option ne "defaults" and $grinderEnabled) { 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"}); 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%)."; Log3 $hash->{NAME}, 3, "Extra Strength :: Grinding [".join(" ", @param)."] to get $cups cups (error rate: $error%).";
} else { } else {
@ -701,7 +770,7 @@ sub SmarterCoffee_Set {
$option = "brew_with_settings"; $option = "brew_with_settings";
$messagePart = $input{cups}.$input{strength}.$input{hotplate}.$input{grinder}; $messagePart = $input{cups}.$input{strength}.$input{hotplate}.$input{grinder};
SmarterCoffee_UpdateReadings($hash, UpdateReadings($hash,
sub($) { sub($) {
my ($updateReading) = @_; my ($updateReading) = @_;
for my $key (keys %readingsValues) { $updateReading->( $key, $readingsValues{$key} ) } 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"}); 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". # 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". # Aborting device update when strength is "extra".
return undef if ($option eq "strength" and $param[0] eq "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". # Resetting internal states before executing "stop".
if ($option eq "stop" and ($param[0] // "") ne "no-reset") { if ($option eq "stop" and ($param[0] // "") ne "no-reset") {
SmarterCoffee_ResetState($hash); ResetState($hash);
} }
$messagePart = $optionToMessage->( $option, $param[0] ); $messagePart = $optionToMessage->( $option, $param[0] );
} }
# Command execution # Command execution
if (defined($SmarterCoffee_Commands{$option})) { if (defined($commands{$option})) {
my $message = $SmarterCoffee_Commands{$option}; my $message = $commands{$option};
# Replacing placeholders with value. # Replacing placeholders with value.
$message =~ s/#+/$messagePart/ if (defined($messagePart) and $messagePart =~ /^[a-f0-9]{2,}$/); $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; $hash->{"PENDING_COMMAND"} = $hash->{".last_command"} = $message;
Log3 $hash->{NAME}, 4, "Connection :: Sending message: $message [".$hash->{".last_set_command"}."]"; 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; return undef;
} elsif ($option eq "disconnect" or $option eq "reconnect") { } elsif ($option eq "disconnect" or $option eq "reconnect") {
# This option is primarily to test if reconnect works. # This option is primarily to test if reconnect works.
DevIo_Disconnected($hash); main::DevIo_Disconnected($hash);
SmarterCoffee_Connect($hash) if ($option eq "reconnect"); Connect($hash) if ($option eq "reconnect");
return undef; return undef;
} elsif ($option ne "?" and $option ne "help") { } elsif ($option ne "?" and $option ne "help") {
@ -773,7 +842,7 @@ sub SmarterCoffee_Set {
} }
my @strength = split(",", "weak,medium,strong,extra"); 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" return "Unknown argument $option, choose one of"
." brew" ." brew"
@ -787,14 +856,14 @@ sub SmarterCoffee_Set {
." hotplate_on_for_minutes:slider,5,5,40"; ." hotplate_on_for_minutes:slider,5,5,40";
} }
sub SmarterCoffee_ResetState($) { sub ResetState($) {
my ($hash) = @_; my ($hash) = @_;
SmarterCoffee_ResetBrewState($hash); ResetBrewState($hash);
SmarterCoffee_ResetExtraStrengthMode($hash); ResetExtraStrengthMode($hash);
} }
sub SmarterCoffee_Notify($$) { sub Notify($$) {
my ($hash, $eventHash) = @_; my ($hash, $eventHash) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $senderName = $eventHash->{NAME}; my $senderName = $eventHash->{NAME};
@ -804,27 +873,27 @@ sub SmarterCoffee_Notify($$) {
if (my $events = deviceEvents($eventHash, 1)) { if (my $events = deviceEvents($eventHash, 1)) {
if ($senderName eq "global") { if ($senderName eq "global") {
SmarterCoffee_ReadConfiguration($hash) if (grep(m/^(INITIALIZED|REREADCFG)$/, @{$events})); ReadConfiguration($hash) if (grep(m/^(INITIALIZED|REREADCFG)$/, @{$events}));
} else { } else {
for (@{$events}) { for (@{$events}) {
if ($_) { if ($_) {
SmarterCoffee_ProcessBrewStateEvents($hash, $_); ProcessBrewStateEvents($hash, $_);
SmarterCoffee_ProcessEventForExtraStrength($hash, $_); ProcessEventForExtraStrength($hash, $_);
SmarterCoffee_LogCommands($hash, $_); LogCommands($hash, $_);
} }
} }
} }
} }
} }
sub SmarterCoffee_ReadConfiguration($$) { sub ReadConfiguration($) {
my ($hash) = @_; my ($hash) = @_;
# Restoring extra strength # Restoring extra strength
$hash->{".extra_strength.enabled"} = 1 if (ReadingsVal($hash->{NAME}, "strength", "") =~ /^extra.*/); $hash->{".extra_strength.enabled"} = 1 if (ReadingsVal($hash->{NAME}, "strength", "") =~ /^extra.*/);
} }
sub SmarterCoffee_LogCommands($$) { sub LogCommands($$) {
my ($hash, $event) = @_; my ($hash, $event) = @_;
if ($event =~ /^last_command_success:\s*(yes|no)\s*$/i and (my $command = ReadingsVal($hash->{NAME}, "last_command", 0))) { 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) = @_; my ($hash, $event) = @_;
# Setting "INITIATED_BREWING" when brewing was initiated by a command (and not by using the machine's buttons) # 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; $hash->{".brew-state"} = $1;
} elsif ($event =~ /^state:\s*done/) { } elsif ($event =~ /^state:\s*done/) {
SmarterCoffee_ResetBrewState($hash); ResetBrewState($hash);
} elsif ($event =~ /^state:\s*(.+)$/ and ($hash->{".brew-state"} // "") =~ /^(brewing|grinding)$/) { } 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."; 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) = @_; my ($hash) = @_;
delete $hash->{".brew-state"} if defined($hash->{".brew-state"}); delete $hash->{".brew-state"} if defined($hash->{".brew-state"});
delete $hash->{"INITIATED_BREWING"} if defined($hash->{"INITIATED_BREWING"}); delete $hash->{"INITIATED_BREWING"} if defined($hash->{"INITIATED_BREWING"});
} }
sub SmarterCoffee_ProcessEventForExtraStrength($$) { sub ProcessEventForExtraStrength($$) {
my ($hash, $event) = @_; my ($hash, $event) = @_;
if ($event =~ /^strength:\s*extra\s*$/) { if ($event =~ /^strength:\s*extra\s*$/) {
# Listen to "set strength extra" and enable it if available. # 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'"; Log3 $hash->{NAME}, 3, "Extra-Strength :: Downgrading strength 'extra' to 'strong'";
fhem("sleep 0.1 fix-strength ; set ".$hash->{NAME}." strength 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" if (ReadingsVal($hash->{NAME}, "grinder", "-") eq "disabled"
and (my $cups = int(ReadingsVal($hash->{NAME}, "cups", 0))) > 0 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 (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'."; 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" ]}); Set($hash, @{[ $hash->{NAME}, "stop" ]});
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "brew", $cups, "extra" ]}); Set($hash, @{[ $hash->{NAME}, "brew", $cups, "extra" ]});
} }
} elsif (($hash->{".extra_strength.enabled"} or $hash->{".extra_strength.phase-2"})) { } elsif (($hash->{".extra_strength.enabled"} or $hash->{".extra_strength.phase-2"})) {
@ -893,22 +962,22 @@ sub SmarterCoffee_ProcessEventForExtraStrength($$) {
} elsif ($event =~ /^state:\s*done/) { } elsif ($event =~ /^state:\s*done/) {
# Finishing first round (grinding & first brew are done here) # Finishing first round (grinding & first brew are done here)
if ((my $delay = int($hash->{".extra_strength.pre_brew_phase_delay"} // 0)) > 0) { 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 { } else {
if (int($hash->{".extra_strength.original_desired_cups"} // 0) > 0) { 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"}) { } elsif ($event =~ /^state:\s*brewing/ and not $hash->{".extra_strength.phase-2"}) {
# Entering phase-2: Brewing after initial grinding at different settings. # 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 ($hash) = @_;
my @params = ( my @params = (
ReadingsVal($hash->{NAME}, "cups", "-"), ReadingsVal($hash->{NAME}, "cups", "-"),
@ -917,12 +986,12 @@ sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
"disabled" "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. # 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) # 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, "brew");
unshift(@params, $hash->{NAME}); unshift(@params, $hash->{NAME});
@ -932,17 +1001,17 @@ sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
: "2"; : "2";
Log3 $hash->{NAME}, 4, "Extra-Strength :: Phase $phase [set ".join(" ", @params)."]"; Log3 $hash->{NAME}, 4, "Extra-Strength :: Phase $phase [set ".join(" ", @params)."]";
SmarterCoffee_Set($hash, @params); Set($hash, @params);
return 1; return 1;
} }
return 0; return 0;
} }
sub SmarterCoffee_IsExtraStrengthModeAvailable($;$) { sub IsExtraStrengthModeAvailable($;$) {
my ($hash, $slient) = @_; 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)); 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) { 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) = @_; my ($hash) = @_;
return 1 if ($hash->{".extra_strength.enabled"}); 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."; Log3 $hash->{NAME}, 4, "Extra-Strength :: Entering extra strength mode.";
$hash->{".extra_strength.enabled"} = 1; $hash->{".extra_strength.enabled"} = 1;
return 1; return 1;
@ -968,7 +1037,7 @@ sub SmarterCoffee_EnableExtraStrengthMode($) {
} }
} }
sub SmarterCoffee_ResetExtraStrengthMode($;$) { sub ResetExtraStrengthMode($;$) {
my ($hash, $partial) = @_; my ($hash, $partial) = @_;
Log3 $hash->{NAME}, 4, ("Extra-Strength :: Resetting state to initial (partial: " . ($partial // 0) . ")."); 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) = @_; my ($hash, $params, $phase) = @_;
return 0 if (not (SmarterCoffee_EnableExtraStrengthMode($hash))); return 0 if (not (EnableExtraStrengthMode($hash)));
if ($phase eq "grind") { 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 @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)) } 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).")"; Log3 $hash->{NAME}, 4, "Extra-Strength :: Reference weights: ".join(" ", @weights)." (".join(" ", @strengths).")";
@ -1059,7 +1128,7 @@ sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
$params->[0] = $preBrewCups; $params->[0] = $preBrewCups;
} else { } else {
$params->[0] = $hash->{".extra_strength.desired_cups"}; $params->[0] = $hash->{".extra_strength.desired_cups"};
SmarterCoffee_ResetExtraStrengthMode($hash, 1); ResetExtraStrengthMode($hash, 1);
} }
return 1; return 1;
@ -1068,12 +1137,12 @@ sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
return 0; return 0;
} }
sub SmarterCoffee_UpdateReading($$$) { sub UpdateReading($$$) {
my ($hash, $name, $value) = @_; 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) = @_; my ($hash, $callback, $forceUpdate) = @_;
$forceUpdate = (($forceUpdate // 0) $forceUpdate = (($forceUpdate // 0)
@ -1099,29 +1168,29 @@ sub SmarterCoffee_UpdateReadings($$;$) {
readingsEndUpdate($hash, ($updated or $forceUpdate)); readingsEndUpdate($hash, ($updated or $forceUpdate));
} }
sub SmarterCoffee_RunDiscoveryProcess($;$) { sub RunDiscoveryProcess($;$) {
my ($hash, $skipConnect) = @_; my ($hash, $skipConnect) = @_;
if (SmarterCoffee_Discover($hash) and not $skipConnect) { if (Discover($hash) and not $skipConnect) {
SmarterCoffee_Connect($hash); Connect($hash);
} }
InternalTimer(gettimeofday() + $SmarterCoffee_DiscoveryInterval, "SmarterCoffee_RunDiscoveryProcess", $hash, 0); InternalTimer(gettimeofday() + $discoveryInterval, "SmarterCoffee::RunDiscoveryProcess", $hash, 0);
} }
sub SmarterCoffee_InetSocketAddressString($) { sub InetSocketAddressString($) {
my ($port, $inetAddress) = sockaddr_in($_[0]); my ($sport, $inetAddress) = sockaddr_in($_[0]);
return inet_ntoa($inetAddress).":$port" return inet_ntoa($inetAddress).":$sport"
} }
sub SmarterCoffee_Discover($) { sub Discover($) {
my ($hash) = @_; my ($hash) = @_;
my $existingDeviceName = ($hash->{DeviceName} // ""); my $existingDeviceName = ($hash->{DeviceName} // "");
my $broadcastAddress = sockaddr_in($SmarterCoffee_Port, INADDR_BROADCAST); my $broadcastAddress = sockaddr_in($port, INADDR_BROADCAST);
Log3 $hash->{NAME}, 4, 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')); socket(my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp'));
setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1); setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1);
@ -1130,17 +1199,17 @@ sub SmarterCoffee_Discover($) {
while ($wait->can_read( 10 )) { while ($wait->can_read( 10 )) {
my $deviceAddress = recv($socket, my $message, 128, 0); my $deviceAddress = recv($socket, my $message, 128, 0);
my $inetSocketAddress = SmarterCoffee_InetSocketAddressString($deviceAddress); my $inetSocketAddress = InetSocketAddressString($deviceAddress);
$message = unpack('H*', $message); $message = unpack('H*', $message);
Log3 $hash->{NAME}, 4, "Discovery :: Received message $message from $inetSocketAddress"; Log3 $hash->{NAME}, 4, "Discovery :: Received message $message from $inetSocketAddress";
if ($message =~ /^65.*7e.*/ and SmarterCoffee_ParseMessage($hash, $message)) { if ($message =~ /^65.*7e.*/ and ParseMessage($hash, $message)) {
my ($port, $inetAddress) = sockaddr_in($deviceAddress); my ($sport, $inetAddress) = sockaddr_in($deviceAddress);
if (my ($hostname) = gethostbyaddr($inetAddress, AF_INET)) { if (my ($hostname) = gethostbyaddr($inetAddress, AF_INET)) {
$hash->{DeviceName} = $hostname.":$port"; $hash->{DeviceName} = $hostname.":$sport";
} else { } else {
$hash->{DeviceName} = $inetSocketAddress; $hash->{DeviceName} = $inetSocketAddress;
} }
@ -1247,7 +1316,7 @@ my $SmarterCoffee_StatusIconSVG = <<XML;
</svg> </svg>
XML XML
sub SmarterCoffee_GetDevStateIcon { sub GetDevStateIcon {
my ($name, $colors) = @_; my ($name, $colors) = @_;
my ($state, $icon) = (Value($name), $SmarterCoffee_StatusIconSVG); my ($state, $icon) = (Value($name), $SmarterCoffee_StatusIconSVG);
@ -1467,14 +1536,14 @@ sub SmarterCoffee_GetDevStateIcon {
<b>Attributes</b><br> <b>Attributes</b><br>
<ul> <ul>
<li> <li>
<code>attr &lt;name&gt; devStateIcon { SmarterCoffee_GetDevStateIcon($name) }</code> <code>attr &lt;name&gt; devStateIcon { SmarterCoffee::GetDevStateIcon($name) }</code>
<br><br> <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. the machine states (ready, brewing, done) and shows information on carafe, hotplate and water level.
<br><br> <br><br>
The icon is monochrome using a default color that may change to highlight states: ready, brewing, done. 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> 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>" 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. sets colors for default, ready, brewing and done.
<br><br> <br><br>
Colors are specified as HTML color values delimited by whitespace using a fixed order of "default ready brewing done". Colors are specified as HTML color values delimited by whitespace using a fixed order of "default ready brewing done".