1580 lines
72 KiB
Perl
1580 lines
72 KiB
Perl
#############################################################
|
|
#
|
|
# Copyright notice
|
|
#
|
|
# (c) 2016
|
|
# Copyright: Juergen Kellerer (juergen at k123 dot eu)
|
|
# All rights reserved
|
|
#
|
|
# This script free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# The GNU General Public License can be found at
|
|
# http://www.gnu.org/copyleft/gpl.html.
|
|
# A copy is found in the textfile GPL.txt and important notices to the license
|
|
# from the author is found in LICENSE.txt distributed with these scripts.
|
|
#
|
|
# This script is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# This copyright notice MUST APPEAR in all copies of the script!
|
|
#
|
|
# $Id$
|
|
#
|
|
#############################################################
|
|
#
|
|
# SmaterCoffee.pm by Juergen Kellerer, 2016
|
|
#
|
|
# FHEM module to communicate with a Smarter Coffee machine
|
|
#
|
|
# Credits:
|
|
# Thanks to creators and contributors of:
|
|
# - https://github.com/nanab/smartercoffee
|
|
# - https://github.com/petermajor/SmartThingsSmarterCoffee
|
|
# - https://github.com/Tristan79/iBrew
|
|
# .. and to all the volonteers crafting the FHEM project.
|
|
#
|
|
# Version: 0.9.1
|
|
#
|
|
#############################################################
|
|
#
|
|
# v0.9.1 - 2017-04-25
|
|
# - fixed "stop" detection interferring with "extra" strength.
|
|
# - added new state "grinding".
|
|
# v0.9 - 2017-04-24
|
|
# - added "strength-extra-start-on-device-strength" which allows
|
|
# brewing with "extra" strength using device buttons.
|
|
# - added "INITIATED_BREWING" internals to see if FHEM started it.
|
|
# - fixed timing problem when forcing "grinder" in extra mode.
|
|
# - fixed "stop" using device button doesn't reset extra mode.
|
|
# - fixed incorrect placement of start and end anchors in event
|
|
# regex leading to a too broad event handling for INITIALIZED
|
|
# events.
|
|
# - fixed "hotplate off" didn't reset state to "ready".
|
|
#
|
|
# v0.8 - 2017-03-18
|
|
# - added "controls.txt" to support automatic updates in FHEM.
|
|
# - changed default value of 'strength extra' to 140% to match
|
|
# 6 gramms coffee per cup by default.
|
|
# - improved dev-state icon and documentation.
|
|
# - fixed possible fallthrough to brew in pre-brew phase.
|
|
# - fixed a problem that strength extra could be started without
|
|
# also enabling grinder.
|
|
# - fixed 'strength extra' not being restored when restarting.
|
|
#
|
|
# v0.7 - 2016-08-28
|
|
# - added 'reset' defaults to factory settings command.
|
|
# - added 'get_defaults'.
|
|
# - added 'strength-extra-pre-brew-*'.
|
|
#
|
|
# v0.6 - 2016-08-20
|
|
# - final updates & cleanup, preparation for initial checking.
|
|
#
|
|
# v0.5 - 2016-08-19
|
|
# - added strength "extra"
|
|
# - added descale detection and descale command
|
|
# - improvements in connection handling
|
|
#
|
|
# v0.4 - 2016-08-17
|
|
# - added "set defaults" command
|
|
# - support reading device type and firmware version
|
|
# - changed status detection from simple mapping to bitmasks
|
|
# - handling carafe required dealing with "ready" state
|
|
#
|
|
# v0.3 - 2016-08-06
|
|
# - added custom state icon (embedded SVG)
|
|
#
|
|
# v0.2 - 2016-07-30
|
|
# - support auto discovery via UPD broadcast
|
|
# - start brewing with custom / default settings
|
|
# - stop brewing while running
|
|
#
|
|
# v0.1 - 2016-07-29
|
|
# - define Smarter Coffee based on fixed IP or hostname
|
|
# - set cups, strength, grinder and toggle hotplate
|
|
# - start brewing
|
|
# - view detailed status
|
|
#
|
|
#############################################################
|
|
|
|
package main;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Data::Dumper;
|
|
use Socket;
|
|
use IO::Select;
|
|
|
|
use DevIo;
|
|
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);
|
|
|
|
my %SmarterCoffee_MessageMaps = (
|
|
status_bitmasks => [
|
|
# BIT 1 = ???
|
|
# BIT 2 = hotplate
|
|
# BIT 3 = idle/heating
|
|
# BIT 4 = brewing/descaling
|
|
# BIT 5 = grinding
|
|
# BIT 6 = ready/done
|
|
# BIT 7 = grinder
|
|
# BIT 8 = carafe
|
|
[ '00000000' => { grinder => "disabled", carafe => "missing", hotplate => "off", state => "maintenance" } ],
|
|
[ '01000000' => { hotplate => "on" } ],
|
|
[ '00000100' => { state => "ready" } ],
|
|
[ '00100000' => { state => "ready" } ], # Set when hotplate is off after being in "heating" state.
|
|
[ '01000100' => { state => "done" } ],
|
|
[ '00001000' => { state => "grinding" } ],
|
|
[ '01010000' => { state => "brewing" } ],
|
|
[ '01100000' => { state => "heating" } ],
|
|
[ '00000010' => { grinder => "enabled" } ],
|
|
[ '00000001' => { carafe => "present" } ],
|
|
],
|
|
|
|
water => {
|
|
# HEX key, only lower 4 bits are used.
|
|
'00' => { water => "none", water_level => 0 },
|
|
'01' => { water => "low", water_level => 25 },
|
|
'02' => { water => "half", water_level => 50 },
|
|
'03' => { water => "full", water_level => 100 },
|
|
},
|
|
|
|
strength => {
|
|
# HEX key, only lower 4 bits are used.
|
|
'00' => { strength => "weak", strength_level => 1 },
|
|
'01' => { strength => "medium", strength_level => 2 },
|
|
'02' => { strength => "strong", strength_level => 3 },
|
|
},
|
|
|
|
cups => {
|
|
# HEX key, only lower 4 bits are used.
|
|
'01' => 1, '02' => 2, '03' => 3, '04' => 4, '05' => 5, '06' => 6,
|
|
'07' => 7, '08' => 8, '09' => 9, '0a' => 10, '0b' => 11, '0c' => 12
|
|
},
|
|
|
|
grinder => {
|
|
'00' => "disabled", '01' => "enabled"
|
|
}
|
|
);
|
|
|
|
my %SmarterCoffee_Commands = (
|
|
reset => "107e",
|
|
brew => "377e",
|
|
brew_with_settings => "33########7e",
|
|
adjust_defaults => "38########7e",
|
|
get_defaults => "487e",
|
|
stop => "347e",
|
|
strength => "35##7e",
|
|
cups => "36##7e",
|
|
grinder => "3c7e",
|
|
hotplate_on_for_minutes => "3e##7e",
|
|
hotplate_off => "4a7e",
|
|
carafe_required_status => "4c7e",
|
|
cups_single_mode_status => "4f7e",
|
|
info => "647e",
|
|
history => "467e"
|
|
);
|
|
|
|
my @SmarterCoffee_GetCommands = ("info", "carafe_required_status", "cups_single_mode_status", "get_defaults"); #, "history"
|
|
|
|
my %SmarterCoffee_ResponseCodes = (
|
|
'00' => { message => 'Ok', success => 'yes' },
|
|
'01' => { message => 'Ok, brewing in progress', success => 'yes' },
|
|
|
|
'04' => { message => 'Ok, stopped', success => 'yes' },
|
|
|
|
'05' => { message => 'No carafe, brewing not possible', success => 'no' },
|
|
'06' => { message => 'No water, brewing not possible', success => 'no' },
|
|
|
|
'69' => { message => 'Invalid command', success => 'no' },
|
|
);
|
|
|
|
|
|
sub SmarterCoffee_ParseMessage {
|
|
my ($hash, $message) = @_;
|
|
|
|
$message = ($hash->{PARTIAL} // "") if (not defined($message));
|
|
|
|
if ($message =~ /^(32|03|47|49|4d|50|65)[0-9a-f]+7e.*/) {
|
|
|
|
Log3 $hash->{NAME}, 5, "Connection :: Received from ".($hash->{DeviceName} // "unknown").": $message";
|
|
|
|
# Handle multiple messages in one frame:
|
|
my @messages = split("7e", $message);
|
|
if (int(@messages) > 1) {
|
|
my $failed;
|
|
for (@messages) {
|
|
$failed = 1 if (not SmarterCoffee_ParseMessage($hash, $_."7e"));
|
|
}
|
|
return not $failed;
|
|
}
|
|
|
|
# Handle single message:
|
|
$hash->{".last_response"} = $message if $message =~ /^(03|49|4d|50|65).+7e.*/;
|
|
|
|
# Parse response of a command.
|
|
if ($message =~ /^03([0-9a-f]{2})7e.*/) {
|
|
if (my $response = ($SmarterCoffee_ResponseCodes{$1} // 0)) {
|
|
SmarterCoffee_UpdateReadings($hash,
|
|
sub($) {
|
|
my ($updateReading) = @_;
|
|
while (my ($key, $value) = each %{$response}) {
|
|
$updateReading->( "last_command_$key", $value );
|
|
}
|
|
$updateReading->( "last_command", $hash->{".last_set_command"} );
|
|
}, 1);
|
|
} else {
|
|
Log3 $hash->{NAME}, 3, "Connection :: Unknown command response '$message'.";
|
|
}
|
|
}
|
|
|
|
# Parse history message.
|
|
if ($message =~ /^47([0-9a-f]{2})(.+)7e.*/) {
|
|
my @history = split("7d", $2);
|
|
|
|
Log 2, Dumper(@history); #TODO
|
|
}
|
|
|
|
# Parse default settings message.
|
|
if ($message =~ /^49([0-9a-f]+)7e.*/) {
|
|
my %values = (
|
|
cups => '0'.substr($1, 1, 1),
|
|
strength => substr($1, 2, 2),
|
|
grinder => '0'.substr($1, 5, 1),
|
|
hotplate => substr($1, 6, 2),
|
|
);
|
|
|
|
SmarterCoffee_ParseStatusValues($hash, \%values);
|
|
DoTrigger($hash->{NAME}, "get_defaults");
|
|
SmarterCoffee_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"));
|
|
}
|
|
|
|
# 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"));
|
|
}
|
|
|
|
# Parse info & discovery message.
|
|
if ($message =~ /^65([0-9a-f]{2})([0-9a-f]{2})7e.*/) {
|
|
$hash->{FIRMWARE} = ord(pack("H2", $2));
|
|
return 0 if ($1 ne "02");
|
|
}
|
|
|
|
# Parse status message.
|
|
if ($message =~ /^(32[0-9a-f]+7e).*/ and $message ne $hash->{".raw_last_status"}) {
|
|
$hash->{".last_status"} = $hash->{".raw_last_status"} = $message = $1;
|
|
|
|
my %values = (
|
|
status => substr($message, 2, 2),
|
|
water => '0'.substr($message, 5, 1),
|
|
strength => substr($message, 8, 2),
|
|
cups => '0'.substr($message, 11, 1),
|
|
);
|
|
|
|
SmarterCoffee_ParseStatusValues($hash, \%values);
|
|
}
|
|
|
|
$hash->{CONNECTION} = ""
|
|
."STATUS: ".($hash->{".last_status"} // "n/a")
|
|
." | COMMAND: ".($hash->{".last_command"} // "n/a")
|
|
." => ".($hash->{".last_response"} // "n/a");
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub SmarterCoffee_DumpToExpression($) {
|
|
my $d = Dumper($_[0]);
|
|
$d =~ s/\s+/ /g;
|
|
$d =~ s/[^\}]*(\{.+\})[^\}]*/$1/;
|
|
return $d;
|
|
}
|
|
|
|
sub SmarterCoffee_ParseStatusValues {
|
|
my ($hash, $values) = @_;
|
|
|
|
while (my ($mappingKey, $rawValue) = each %{$values}) {
|
|
if ($mappingKey eq "status") {
|
|
my %status = ();
|
|
my $unpackedStatusBits = sprintf('%08b', ord(pack("H2", $rawValue)));
|
|
$hash->{".last_status"} .= " ($unpackedStatusBits)";
|
|
|
|
for (@{$SmarterCoffee_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);
|
|
}
|
|
}
|
|
$values->{$mappingKey} = { %status };
|
|
} else {
|
|
if (defined($SmarterCoffee_MessageMaps{$mappingKey}{$rawValue})) {
|
|
$values->{$mappingKey} = $SmarterCoffee_MessageMaps{$mappingKey}{$rawValue};
|
|
} elsif ($mappingKey eq "hotplate") {
|
|
$values->{$mappingKey} = { "hotplate_on_for_minutes" => hex($rawValue) };
|
|
} else {
|
|
Log3 $hash->{NAME}, 3, "Connection :: Unknown value '$rawValue' for $mappingKey message part.";
|
|
$values->{$mappingKey} = { };
|
|
}
|
|
}
|
|
}
|
|
|
|
Log3 $hash->{NAME}, 5, "Connection :: Parsed message: ".Dumper($values);
|
|
|
|
SmarterCoffee_UpdateReadings($hash,
|
|
sub($) {
|
|
my ($updateReading) = @_;
|
|
my $state = 0;
|
|
|
|
while (my ($n, $readings) = each %{$values}) {
|
|
$readings = { $n => $readings } if (ref($readings) ne "HASH");
|
|
|
|
while (my ($name, $value) = each %{$readings}) {
|
|
if ($name eq "state") {
|
|
$state = $value;
|
|
} else {
|
|
$updateReading->( $name, $value );
|
|
}
|
|
}
|
|
}
|
|
|
|
# Adding calculated readings
|
|
if (defined($values->{"water"}) and (my $maxCups = int(($values->{"water"}{"water_level"} // 0) / 100 * 12))) {
|
|
$maxCups = 3 if ($maxCups > 3 and ReadingsVal($hash->{NAME}, "cups_single_mode", "") eq "yes");
|
|
$updateReading->( "cups_max", $maxCups );
|
|
}
|
|
|
|
# Overriding "ready" state if carafe or water is missing.
|
|
if ($state eq "ready") {
|
|
my $cupsOk = (AttrVal($hash->{NAME}, "ignore-max-cups", 1)
|
|
or (ReadingsNum($hash->{NAME}, "cups_max", 0) >= ReadingsNum($hash->{NAME}, "cups", 0)));
|
|
|
|
my $carafeOk = (ReadingsVal($hash->{NAME}, "carafe_required", "yes") ne "yes"
|
|
or (($values->{"status"}{"carafe"} // "") eq "present"));
|
|
|
|
my $waterOk = (($values->{"water"}{"water_level"} // 0) > 0);
|
|
|
|
$state = "maintenance" if (not $carafeOk or not $waterOk or not $cupsOk);
|
|
}
|
|
|
|
# Setting status at last when all other readings are updated.
|
|
$updateReading->( "state", $state ) if $state;
|
|
}
|
|
);
|
|
}
|
|
|
|
sub SmarterCoffee_Connect($) {
|
|
my ($hash) = @_;
|
|
|
|
my $isNewConnection = $hash->{STATE} eq "initializing";
|
|
|
|
RemoveInternalTimer($hash);
|
|
$hash->{STATE} = "disconnected";
|
|
delete $hash->{INVALID_DEVICE} if defined($hash->{INVALID_DEVICE});
|
|
|
|
if ($hash->{AUTO_DETECT}) {
|
|
SmarterCoffee_RunDiscoveryProcess($hash, 1);
|
|
}
|
|
|
|
if (defined($hash->{DeviceName})) {
|
|
if (not ($hash->{DeviceName} =~ m/^(.+):([0-9]+)$/)) {
|
|
$hash->{DeviceName} .= ":$SmarterCoffee_Port";
|
|
}
|
|
|
|
DevIo_CloseDev($hash) if DevIo_IsOpen($hash);
|
|
delete $hash->{DevIoJustClosed} if ($hash->{DevIoJustClosed});
|
|
|
|
SmarterCoffee_ReConnectTimer($hash);
|
|
return SmarterCoffee_OpenIfRequiredAndWritePending($hash, $isNewConnection);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub SmarterCoffee_OpenIfRequiredAndWritePending($;$) {
|
|
my ($hash, $initial) = @_;
|
|
return DevIo_OpenDev($hash, ($initial ? 0 : 1), "SmarterCoffee_WritePending");
|
|
}
|
|
|
|
sub SmarterCoffee_HandleInitialConnectState($) {
|
|
my ($hash) = @_;
|
|
|
|
return if ($hash->{".initial-connection-state"});
|
|
|
|
if (DevIo_IsOpen($hash) and ($hash->{STATE} eq "disconnected" or $hash->{STATE} eq "opened")) {
|
|
$hash->{".initial-connection-state"} = 1;
|
|
|
|
$hash->{STATE} = "connected";
|
|
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" ]});
|
|
|
|
delete $hash->{".initial-connection-state"};
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_WritePending {
|
|
my ($hash, $mustSucceed) = @_;
|
|
|
|
if (DevIo_IsOpen($hash)) {
|
|
my $pending = ($hash->{PENDING_COMMAND} // 0);
|
|
|
|
# Handling initial call on a fresh connection
|
|
SmarterCoffee_HandleInitialConnectState($hash);
|
|
|
|
# Processing pending commands
|
|
if (($hash->{INVALID_DEVICE} // "0") eq "1") {
|
|
$hash->{STATE} = "invalid";
|
|
} else {
|
|
if ($pending) {
|
|
delete $hash->{PENDING_COMMAND} if defined($hash->{PENDING_COMMAND});
|
|
|
|
Log3 $hash->{NAME}, 4, "Connection :: Sending to ".$hash->{DeviceName}.": $pending";
|
|
DevIo_SimpleWrite($hash, $pending, 1);
|
|
$hash->{".raw_last_status"} = "";
|
|
|
|
my $result = DevIo_SimpleReadWithTimeout($hash, 5);
|
|
if ($result) {
|
|
$result = SmarterCoffee_Read($hash, $result);
|
|
} else {
|
|
DevIo_Disconnected($hash);
|
|
}
|
|
|
|
$hash->{INVALID_DEVICE} = "1" if ($mustSucceed and not $result);
|
|
$hash->{PENDING_COMMAND} = $pending if (not $result);
|
|
}
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub SmarterCoffee_Read($;$) {
|
|
my ($hash, $buffer) = @_;
|
|
|
|
# Handle case that fhem reconnected a broken connection and state is "opened".
|
|
SmarterCoffee_HandleInitialConnectState($hash) if (not defined($buffer));
|
|
|
|
# Abort read if we already detected that the device is invalid.
|
|
return 0 if ($hash->{INVALID_DEVICE} // 0);
|
|
|
|
# Reset partial data buffer if it exceeds length of 512 (256 bytes) or when $buffer was specified explicitly.
|
|
$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));
|
|
return 0 if (not defined($buffer));
|
|
|
|
# Appending message bytes as hex string.
|
|
$hash->{PARTIAL} .= unpack('H*', $buffer);
|
|
|
|
# Parsing the message and populate readings.
|
|
if ($hash->{PARTIAL} ne "") {
|
|
if (SmarterCoffee_ParseMessage($hash)) {
|
|
delete $hash->{PARTIAL};
|
|
} else {
|
|
Log3 $hash->{NAME}, 2, "Connection :: Failed parsing buffer content: ".$hash->{PARTIAL};
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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($$) {
|
|
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.*";
|
|
}
|
|
|
|
$attr{$name}{devStateIcon} = '{ SmarterCoffee_GetDevStateIcon($name) }' if not defined($attr{$name}{devStateIcon});
|
|
|
|
if (int(@param) < 3) {
|
|
$hash->{AUTO_DETECT} = 1;
|
|
} else {
|
|
delete $hash->{AUTO_DETECT};
|
|
$hash->{DeviceName} = $param[2];
|
|
}
|
|
|
|
$hash->{NOTIFYDEV} = "global,$name";
|
|
$hash->{STATE} = "initializing";
|
|
$hash->{devioLoglevel} = 4;
|
|
|
|
$hash->{".last_command"} =
|
|
$hash->{".last_response"} =
|
|
$hash->{".last_status"} =
|
|
$hash->{".raw_last_status"} = "";
|
|
|
|
RemoveInternalTimer($hash);
|
|
|
|
SmarterCoffee_Connect($hash);
|
|
|
|
Log3 $hash->{NAME}, 4, "Instance :: Defined module 'SmarterCoffee': ".Dumper($hash);
|
|
}
|
|
|
|
|
|
sub SmarterCoffee_Undefine($$) {
|
|
my ($hash, $arg) = @_;
|
|
|
|
RemoveInternalTimer($hash);
|
|
DevIo_CloseDev($hash);
|
|
|
|
Log3 $hash->{NAME}, 4, "Instance :: Closed module 'SmarterCoffee': ".Dumper($hash);
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub SmarterCoffee_Get {
|
|
my ($hash, @param) = @_;
|
|
|
|
if (grep {$_ eq ($param[1] // "")} @SmarterCoffee_GetCommands) {
|
|
return SmarterCoffee_Set($hash, @param) // "Ok :: ".$hash->{".last_response"};
|
|
} else {
|
|
return "Unknown argument $param[1], choose one of ".join(":noArg ", @SmarterCoffee_GetCommands).":noArg";
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_Set {
|
|
my ($hash, @param) = @_;
|
|
|
|
my $desiredCups = defined($hash->{".extra_strength.original_desired_cups"})
|
|
? $hash->{".extra_strength.original_desired_cups"}
|
|
: ReadingsVal($hash->{NAME}, "cups", 1);
|
|
|
|
my $optionToMessage = sub($$;$) {
|
|
my ($option, $optionValue, $value) = @_;
|
|
|
|
# Remembering "cups" when a message part is looked-up.
|
|
$desiredCups = int($optionValue) if ($option eq "cups" and $optionValue =~ /^[0-9]+$/);
|
|
|
|
# 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}));
|
|
$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));
|
|
}
|
|
|
|
$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});
|
|
|
|
SmarterCoffee_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)) {
|
|
# 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};
|
|
if ((ref($v) eq "HASH" ? grep(/^$optionValue$/, values %{$v}) : $v eq $optionValue)) {
|
|
return $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
};
|
|
|
|
# Command & params pre-processing
|
|
my ($instanceName, $option, $messagePart) = (shift @param, shift @param, undef);
|
|
|
|
# Support "set <name> off"
|
|
$option = "stop" if ($option =~ /^off$/i);
|
|
|
|
# Support "set <name> on" and "set <name> start"
|
|
if (($option =~ /^on$/i and AttrVal($hash->{NAME}, "set-on-brews-coffee", "0") =~ /^(yes|true|1)$/i) or $option =~ /^start$/i) {
|
|
$option = "brew";
|
|
}
|
|
|
|
# Support "set 6-cups" as alias to "set brew 6" (for better readable webCmds)
|
|
if ($option =~ /^([0-9]+)-cups(|[\-_,:;][a-z]+)$/i) {
|
|
unshift(@param, substr($2, 1)) if ($2); # supporting "set 3-cups,strong"
|
|
unshift(@param, $1);
|
|
$option = "brew";
|
|
}
|
|
|
|
if ($option eq "brew" or $option eq "defaults") {
|
|
# Handle extra strong coffee
|
|
if (($param[1] // ReadingsVal($hash->{NAME}, "strength", "")) =~ /^extra.*/) {
|
|
# 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" ]});
|
|
$grinderEnabled = 1;
|
|
$param[3] = "enabled" if defined($param[3]);
|
|
}
|
|
|
|
if ($option ne "defaults" and $grinderEnabled) {
|
|
if (SmarterCoffee_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 {
|
|
return "strength 'extra' failed. check water level.";
|
|
}
|
|
} else {
|
|
Log3 $hash->{NAME}, 3, "Extra Strength :: Downgrading strength extra to 'strong' for set $option.";
|
|
$param[1] = "strong";
|
|
}
|
|
}
|
|
|
|
# Handle normal brew or set defaults
|
|
if (not defined($param[0]) or $param[0] ne "current") {
|
|
# Get message parts
|
|
my %input = ("cups" => $param[0], "strength" => $param[1], "hotplate" => $param[2], "grinder" => $param[3]);
|
|
my %readingsValues = ( %input );
|
|
my $inputsDefined = 0;
|
|
|
|
for my $key (keys %input) {
|
|
# Translating input to message part.
|
|
$input{$key} = $optionToMessage->( $key, $input{$key} ) if defined($input{$key});
|
|
|
|
# Taking message part from readings if input didn't specifiy it (using "on" for "hotplate" as the reading would be missleading.
|
|
if (not defined($input{$key})) {
|
|
$readingsValues{$key} = ($key eq "hotplate"
|
|
? (ReadingsVal($hash->{NAME}, "cups_single_mode", "") eq "yes" ? 0 : "on")
|
|
: ReadingsVal($hash->{NAME}, $key, ""));
|
|
|
|
$input{$key} = $optionToMessage->( $key, $readingsValues{$key} );
|
|
}
|
|
|
|
# Count if message part was retrieved
|
|
$inputsDefined++ if defined($input{$key});
|
|
}
|
|
|
|
if ($inputsDefined == 4) {
|
|
if ($option eq "defaults") {
|
|
$option = "adjust_defaults";
|
|
$messagePart = $input{strength}.$input{cups}.$input{grinder}.$input{hotplate};
|
|
} else {
|
|
$option = "brew_with_settings";
|
|
$messagePart = $input{cups}.$input{strength}.$input{hotplate}.$input{grinder};
|
|
|
|
SmarterCoffee_UpdateReadings($hash,
|
|
sub($) {
|
|
my ($updateReading) = @_;
|
|
for my $key (keys %readingsValues) { $updateReading->( $key, $readingsValues{$key} ) }
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Aborting if option "defaults" was not properly prepared to "adjust_defaults".
|
|
return undef if $option eq "defaults";
|
|
|
|
} elsif ($option =~ /^hotplate.*/) {
|
|
if (not defined($param[0])) {
|
|
$param[0] = ($option ne "hotplate_on_for_minutes" and ReadingsVal($hash->{NAME}, "hotplate", "") ne "off") ? "off" : "on";
|
|
}
|
|
$option = $param[0] =~ /^(0|no|disable|off).*$/i ? "hotplate_off" : "hotplate_on_for_minutes";
|
|
$messagePart = $optionToMessage->( $option, $param[0], $param[1] );
|
|
|
|
} else {
|
|
if (defined($param[0])) {
|
|
# Resetting "extra" strength mode when strength is updated.
|
|
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)$/);
|
|
|
|
# Aborting device update when strength is "extra".
|
|
return undef if ($option eq "strength" and $param[0] eq "extra");
|
|
|
|
# Aborting device update if grinder is not changed (every command sent to the coffee machine flips the grinder setting).
|
|
return undef if ($option eq "grinder" and $param[0] eq ReadingsVal($hash->{NAME}, "grinder", ""));
|
|
}
|
|
|
|
# Resetting internal states before executing "stop".
|
|
if ($option eq "stop" and ($param[0] // "") ne "no-reset") {
|
|
SmarterCoffee_ResetState($hash);
|
|
}
|
|
|
|
$messagePart = $optionToMessage->( $option, $param[0] );
|
|
}
|
|
|
|
# Command execution
|
|
if (defined($SmarterCoffee_Commands{$option})) {
|
|
my $message = $SmarterCoffee_Commands{$option};
|
|
|
|
# Replacing placeholders with value.
|
|
$message =~ s/#+/$messagePart/ if (defined($messagePart) and $messagePart =~ /^[a-f0-9]{2,}$/);
|
|
|
|
if ($message =~ /.*#.*/) {
|
|
return "Option $option: Unsupported params: ".join(" ", @param);
|
|
} else {
|
|
$hash->{".last_set_command"} = $option.(int(@param) ? " ".join(" ", @param) : "");
|
|
$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"));
|
|
}
|
|
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");
|
|
return undef;
|
|
|
|
} elsif ($option ne "?" and $option ne "help") {
|
|
return "Unknown option: $option with params: ".join(" ", @param)
|
|
}
|
|
|
|
my @strength = split(",", "weak,medium,strong,extra");
|
|
pop(@strength) if (not SmarterCoffee_IsExtraStrengthModeAvailable($hash));
|
|
|
|
return "Unknown argument $option, choose one of"
|
|
." brew"
|
|
." defaults"
|
|
." reset:noArg"
|
|
." stop:noArg"
|
|
." strength:".join(",", @strength)
|
|
." cups:slider,1,1,12"
|
|
." grinder:enabled,disabled"
|
|
." hotplate"
|
|
." hotplate_on_for_minutes:slider,5,5,40";
|
|
}
|
|
|
|
sub SmarterCoffee_ResetState($) {
|
|
my ($hash) = @_;
|
|
|
|
SmarterCoffee_ResetBrewState($hash);
|
|
SmarterCoffee_ResetExtraStrengthMode($hash);
|
|
}
|
|
|
|
sub SmarterCoffee_Notify($$) {
|
|
my ($hash, $eventHash) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $senderName = $eventHash->{NAME};
|
|
|
|
# Return without any further action if the module is disabled or the event is not from this module or global.
|
|
return "" if (IsDisabled($name) or ($senderName ne $name and $senderName ne "global"));
|
|
|
|
if (my $events = deviceEvents($eventHash, 1)) {
|
|
if ($senderName eq "global") {
|
|
SmarterCoffee_ReadConfiguration($hash) if (grep(m/^(INITIALIZED|REREADCFG)$/, @{$events}));
|
|
} else {
|
|
for (@{$events}) {
|
|
if ($_) {
|
|
SmarterCoffee_ProcessBrewStateEvents($hash, $_);
|
|
SmarterCoffee_ProcessEventForExtraStrength($hash, $_);
|
|
SmarterCoffee_LogCommands($hash, $_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_ReadConfiguration($) {
|
|
my ($hash) = @_;
|
|
|
|
# Restoring extra strength
|
|
$hash->{".extra_strength.enabled"} = 1 if (ReadingsVal($hash->{NAME}, "strength", "") =~ /^extra.*/);
|
|
}
|
|
|
|
sub SmarterCoffee_LogCommands($$) {
|
|
my ($hash, $event) = @_;
|
|
|
|
if ($event =~ /^last_command_success:\s*(yes|no)\s*$/i and (my $command = ReadingsVal($hash->{NAME}, "last_command", 0))) {
|
|
my $message = ReadingsVal($hash->{NAME}, "last_command_message", "");
|
|
if ($1 eq "yes") {
|
|
Log3 $hash->{NAME}, 4, "Command :: Success [$command]; Message: $message";
|
|
} else {
|
|
Log3 $hash->{NAME}, 4, "Command :: Failed [$command]; Cause: $message";
|
|
}
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_ProcessBrewStateEvents($$) {
|
|
my ($hash, $event) = @_;
|
|
|
|
# Setting "INITIATED_BREWING" when brewing was initiated by a command (and not by using the machine's buttons)
|
|
if ($event =~ /^last_command_success:\s*yes\s*$/i and ReadingsVal($hash->{NAME}, "last_command", 0) =~ /^brew.*/) {
|
|
$hash->{"INITIATED_BREWING"} = 1;
|
|
$hash->{".brew-state"} = "brewing";
|
|
|
|
} elsif ($event =~ /^state:\s*(brewing|grinding)/) {
|
|
$hash->{".brew-state"} = $1;
|
|
|
|
} elsif ($event =~ /^state:\s*done/) {
|
|
SmarterCoffee_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);
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_ResetBrewState($) {
|
|
my ($hash) = @_;
|
|
delete $hash->{".brew-state"} if defined($hash->{".brew-state"});
|
|
delete $hash->{"INITIATED_BREWING"} if defined($hash->{"INITIATED_BREWING"});
|
|
}
|
|
|
|
sub SmarterCoffee_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))) {
|
|
Log3 $hash->{NAME}, 3, "Extra-Strength :: Downgrading strength 'extra' to 'strong'";
|
|
fhem("sleep 0.1 fix-strength ; set ".$hash->{NAME}." strength strong");
|
|
}
|
|
|
|
} elsif ($event =~ /^state:\s*brewing/ and not $hash->{"INITIATED_BREWING"}) {
|
|
# Monitor event that brewing was started on the device without grinder and upgrade to 'extra' if configured in attributes.
|
|
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) ) {
|
|
|
|
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" ]});
|
|
}
|
|
|
|
} elsif (($hash->{".extra_strength.enabled"} or $hash->{".extra_strength.phase-2"})) {
|
|
# Listen to "set strength ?" while in extra mode and revert it to extra shortly.
|
|
if ($event =~ /^strength:\s*([^\s]+)\s*$/) {
|
|
fhem("sleep 0.1 fix-strength ; set ".$hash->{NAME}." strength extra");
|
|
|
|
} 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);
|
|
} else {
|
|
if (int($hash->{".extra_strength.original_desired_cups"} // 0) > 0) {
|
|
SmarterCoffee_Set($hash, @{[ $hash->{NAME}, "cups", $hash->{".extra_strength.original_desired_cups"} ]});
|
|
}
|
|
SmarterCoffee_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);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_ExtraStrengthHandleBrewing($) {
|
|
my ($hash) = @_;
|
|
my @params = (
|
|
ReadingsVal($hash->{NAME}, "cups", "-"),
|
|
ReadingsVal($hash->{NAME}, "strength", "-"),
|
|
ReadingsVal($hash->{NAME}, "hotplate_on_for_minutes", (ReadingsVal($hash->{NAME}, "cups_single_mode", "") eq "yes" ? 0 : "on")),
|
|
"disabled"
|
|
);
|
|
|
|
if (SmarterCoffee_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);
|
|
|
|
# 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"};
|
|
|
|
unshift(@params, "brew");
|
|
unshift(@params, $hash->{NAME});
|
|
|
|
my $phase = int($hash->{".extra_strength.pre_brew_phase_delay"} // 0) > 0
|
|
? "2 (pre brew)"
|
|
: "2";
|
|
Log3 $hash->{NAME}, 4, "Extra-Strength :: Phase $phase [set ".join(" ", @params)."]";
|
|
|
|
SmarterCoffee_Set($hash, @params);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub SmarterCoffee_IsExtraStrengthModeAvailable($;$) {
|
|
my ($hash, $slient) = @_;
|
|
|
|
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $SmarterCoffee_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) {
|
|
return 1;
|
|
} else {
|
|
Log3 $hash->{NAME}, (($slient // 1) ? 5 : 3),
|
|
"Extra-Strength :: Strength 'extra' is disabled as [strength-extra-percent = $extraPercent] is out of range (0 < x < 2.5)";
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_EnableExtraStrengthMode($) {
|
|
my ($hash) = @_;
|
|
|
|
return 1 if ($hash->{".extra_strength.enabled"});
|
|
|
|
if (SmarterCoffee_IsExtraStrengthModeAvailable($hash, 0)) {
|
|
Log3 $hash->{NAME}, 4, "Extra-Strength :: Entering extra strength mode.";
|
|
$hash->{".extra_strength.enabled"} = 1;
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_ResetExtraStrengthMode($;$) {
|
|
my ($hash, $partial) = @_;
|
|
|
|
Log3 $hash->{NAME}, 4, ("Extra-Strength :: Resetting state to initial (partial: " . ($partial // 0) . ").");
|
|
foreach my $key ( keys %{$hash} ) {
|
|
my $resetableKey = ($key =~ /^\.extra_strength\..+$/ and $key ne ".extra_strength.enabled");
|
|
|
|
if (($partial // 0) and $resetableKey) {
|
|
$resetableKey = (not $key =~ /.+\.(original_desired_cups|desired_cups|pre_brew_phase_delay|phase-2).+$/);
|
|
}
|
|
|
|
if ($resetableKey) {
|
|
Log3 $hash->{NAME}, 5, "Extra-Strength :: Resetting $key";
|
|
delete $hash->{$key};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub SmarterCoffee_TranslateParamsForExtraStrength($$$) {
|
|
my ($hash, $params, $phase) = @_;
|
|
|
|
return 0 if (not (SmarterCoffee_EnableExtraStrengthMode($hash)));
|
|
|
|
if ($phase eq "grind") {
|
|
my $extraPercent = AttrVal($hash->{NAME}, "strength-extra-percent", $SmarterCoffee_StrengthExtraDefaultPercent);
|
|
|
|
my @strengths = ("weak", "medium", "strong");
|
|
my @weights = split(/\s+/, AttrVal($hash->{NAME}, "strength-coffee-weights", $SmarterCoffee_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).")";
|
|
|
|
my $desiredCups = $params->[0] // ReadingsVal($hash->{NAME}, "cups", 0);
|
|
my $maxCups = ReadingsVal($hash->{NAME}, "cups_max", $desiredCups);
|
|
$desiredCups = $maxCups if ($desiredCups > $maxCups);
|
|
|
|
my %grind = ( "cups" => undef, "desired" => $desiredCups, "strength" => undef, "delta" => undef, "error" => undef );
|
|
|
|
while ($desiredCups > 0 and not defined($grind{cups})) {
|
|
my $targetWeight = $desiredCups * $weights[2] * $extraPercent;
|
|
|
|
for (my $i = 0; $i < int(@weights); $i++) {
|
|
if (($weights[$i] // -1) > 0 and $targetWeight > 0) {
|
|
my $cups = int($targetWeight / $weights[$i]) + ($extraPercent > 1 ? 1 : 0);
|
|
my $weight = $cups * $weights[$i];
|
|
my $delta = ($targetWeight > $weight ? ($targetWeight - $weight) : ($weight - $targetWeight));
|
|
my $error = int((1 - (($targetWeight - $delta) / $targetWeight)) * 100);
|
|
|
|
Log3 $hash->{NAME}, 4,
|
|
"Extra-Strength :: GC: $cups (".$strengths[$i]."), DC: $desiredCups, D: $delta (e:$error%), W: $weight, T: $targetWeight";
|
|
|
|
if ($cups <= $maxCups and (not defined($grind{delta}) or $grind{delta} > $delta)) {
|
|
$grind{desired} = $desiredCups;
|
|
$grind{cups} = $cups;
|
|
$grind{delta} = $delta;
|
|
$grind{strength} = $strengths[$i];
|
|
$grind{error} = $error;
|
|
}
|
|
}
|
|
}
|
|
|
|
$desiredCups--;
|
|
}
|
|
|
|
if (defined($grind{cups})) {
|
|
$hash->{".extra_strength.original_desired_cups"} = $grind{desired};
|
|
$hash->{".extra_strength.desired_cups"} = $grind{desired};
|
|
$hash->{".extra_strength.error_rate"} = $grind{error};
|
|
$params->[0] = $grind{cups};
|
|
$params->[1] = $grind{strength};
|
|
|
|
return 1;
|
|
} else {
|
|
Log3 $hash->{NAME}, 2, "Extra-Strength :: Failed calculating extra strength (not enough water?). Ordinary coffee strength will be applied.";
|
|
}
|
|
|
|
} elsif ($phase eq "brew" and defined($hash->{".extra_strength.desired_cups"})) {
|
|
my ($preBrewCups, $preBrewDelay) = (
|
|
int(AttrVal($hash->{NAME}, "strength-extra-pre-brew-cups", 1)),
|
|
int(AttrVal($hash->{NAME}, "strength-extra-pre-brew-delay-seconds", 0))
|
|
);
|
|
|
|
if ($preBrewCups > 0 and $preBrewDelay > 0
|
|
and $preBrewCups < $hash->{".extra_strength.desired_cups"}
|
|
and not $hash->{".extra_strength.pre_brew_phase_delay"}) {
|
|
|
|
$hash->{".extra_strength.pre_brew_phase_delay"} = $preBrewDelay;
|
|
$hash->{".extra_strength.desired_cups"} -= $preBrewCups;
|
|
$params->[0] = $preBrewCups;
|
|
} else {
|
|
$params->[0] = $hash->{".extra_strength.desired_cups"};
|
|
SmarterCoffee_ResetExtraStrengthMode($hash, 1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub SmarterCoffee_UpdateReading($$$) {
|
|
my ($hash, $name, $value) = @_;
|
|
SmarterCoffee_UpdateReadings($hash, sub($) { ($_[0])->( $name, $value ) });
|
|
}
|
|
|
|
sub SmarterCoffee_UpdateReadings($$;$) {
|
|
my ($hash, $callback, $forceUpdate) = @_;
|
|
|
|
$forceUpdate = (($forceUpdate // 0)
|
|
or defined(AttrVal($hash->{NAME}, "event-on-update-reading", undef))
|
|
or defined(AttrVal($hash->{NAME}, "event-on-change-reading", undef)));
|
|
|
|
my $updated = 0;
|
|
my $updater = sub {
|
|
my ($name, $value) = @_;
|
|
return if not (defined($name) and defined($value));
|
|
|
|
my $changed = ReadingsVal($hash->{NAME}, $name, "##undefined") ne $value;
|
|
if ($changed or $forceUpdate) {
|
|
readingsBulkUpdate($hash, $name, $value);
|
|
$updated = 1 if ($changed);
|
|
}
|
|
|
|
$updated = 1 if ($name eq "state" and $hash->{STATE} ne $value);
|
|
};
|
|
|
|
readingsBeginUpdate($hash);
|
|
$callback->( $updater );
|
|
readingsEndUpdate($hash, ($updated or $forceUpdate));
|
|
}
|
|
|
|
sub SmarterCoffee_RunDiscoveryProcess($;$) {
|
|
my ($hash, $skipConnect) = @_;
|
|
|
|
if (SmarterCoffee_Discover($hash) and not $skipConnect) {
|
|
SmarterCoffee_Connect($hash);
|
|
}
|
|
|
|
InternalTimer(gettimeofday() + $SmarterCoffee_DiscoveryInterval, "SmarterCoffee_RunDiscoveryProcess", $hash, 0);
|
|
}
|
|
|
|
sub SmarterCoffee_InetSocketAddressString($) {
|
|
my ($port, $inetAddress) = sockaddr_in($_[0]);
|
|
return inet_ntoa($inetAddress).":$port"
|
|
}
|
|
|
|
sub SmarterCoffee_Discover($) {
|
|
my ($hash) = @_;
|
|
|
|
my $existingDeviceName = ($hash->{DeviceName} // "");
|
|
my $broadcastAddress = sockaddr_in($SmarterCoffee_Port, INADDR_BROADCAST);
|
|
|
|
Log3 $hash->{NAME}, 4,
|
|
"Discovery :: Broadcasting discovery request to ".SmarterCoffee_InetSocketAddressString($broadcastAddress)." (already discovered: $existingDeviceName)";
|
|
|
|
socket(my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp'));
|
|
setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1);
|
|
send($socket, 'd~', 0, $broadcastAddress);
|
|
my $wait = IO::Select->new( $socket );
|
|
|
|
while ($wait->can_read( 10 )) {
|
|
my $deviceAddress = recv($socket, my $message, 128, 0);
|
|
my $inetSocketAddress = SmarterCoffee_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 (my ($hostname) = gethostbyaddr($inetAddress, AF_INET)) {
|
|
$hash->{DeviceName} = $hostname.":$port";
|
|
} else {
|
|
$hash->{DeviceName} = $inetSocketAddress;
|
|
}
|
|
|
|
if ($existingDeviceName ne $hash->{DeviceName}) {
|
|
Log3 $hash->{NAME}, 3, "Discovery :: Discovered smarter coffee machine (message=$message): ".$hash->{DeviceName};
|
|
}
|
|
|
|
last;
|
|
}
|
|
}
|
|
|
|
close $socket;
|
|
|
|
if (!defined($hash->{DeviceName})) {
|
|
my $recommendation = "Recommendation: Specify <IP address or hostname> in fhem config at: "
|
|
."'define ".$hash->{NAME}." SmarterCoffee <IP address or hostname>' or check network / coffee machine.";
|
|
Log3 $hash->{NAME}, 2, "Discovery :: Failed discovering smarter coffee machine. $recommendation";
|
|
return 0;
|
|
} else {
|
|
return $existingDeviceName ne $hash->{DeviceName};
|
|
}
|
|
}
|
|
|
|
## -------------------------------------------------------------------------------------------------------------------
|
|
## The following section deals with displaying dev state (it is completely optional with regards to the functionality)
|
|
|
|
my $SmarterCoffee_StatusIconSVG = <<XML;
|
|
<svg class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0,0,340,300">
|
|
<g id="Layer1" name="water-level-25" opacity="1">
|
|
<path id="shapePath1" d="M10.7664,202.592 L39.25,202.592 L39.25,251.046 L10.7664,251.046 L10.7664,202.592 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill-rule:evenodd;fill:#000000;fill-opacity:0.501961;"/>
|
|
</g>
|
|
<g id="Layer2" name="water-level-50" opacity="1">
|
|
<path id="shapePath2" d="M10.7664,148.916 L39.25,148.916 L39.25,251.046 L10.7664,251.046 L10.7664,148.916 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill-rule:evenodd;fill:#000000;fill-opacity:0.501961;"/>
|
|
</g>
|
|
<g id="Layer3" name="water-level-100" opacity="1">
|
|
<path id="shapePath3" d="M10.7664,72.7703 L39.25,72.7703 L39.25,251.046 L10.7664,251.046 L10.7664,72.7703 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:0;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill-rule:evenodd;fill:#000000;fill-opacity:0.501961;"/>
|
|
</g>
|
|
<g id="Layer4" name="coffee-level" opacity="1">
|
|
<path id="shapePath4"
|
|
d="M105.839,186.865 C110.989,185.046 119.097,178.364 126.436,179.589 C135.267,181.064 134.469,186.36 143.482,186.592 C151.348,186.796 156.278,181.403 164.079,180.563 C173.196,179.579 180.365,188.579 189.536,189.158 C196.588,189.602 208.142,183.427 215.216,183.427 C224.061,183.427 226.257,189.158 235.103,189.158 C241.477,189.158 248.419,184.86 252.858,183.427 "
|
|
style="stroke:#000000;stroke-opacity:0.776471;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath5" d="M119.439,200.823 L121.792,200.823 C127.621,198.887 129.259,210.763 130.009,202.074 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath6" d="M164.019,197.487 C166.354,198.01 171.011,196.425 171.832,198.321 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath7" d="M221.007,204.159 C223.591,204.456 227.478,203.571 229.28,204.576 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath8" d="M210.437,222.922 L221.467,222.922 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath9" d="M230.659,241.269 C226.311,241.752 216.061,240.957 216.411,238.35 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath10" d="M120.358,235.432 C123.681,235.753 128.403,234.804 130.928,235.849 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath11" d="M158.963,226.548 L170.913,226.548 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath12" d="M192.513,232.513 Z"
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath13" d="M200.326,196.236 C202.87,195.729 207.125,195.933 208.139,196.236 "
|
|
style="stroke:#000000;stroke-opacity:0.509804;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
<g id="Layer5" name="machine" opacity="1">
|
|
<path id="shapePath14"
|
|
d="M10.7795,288.06 L309.337,288.06 L309.337,287.83 L63.516,287.83 L63.516,38.3877 L277.337,38.3877 L291.337,10.509 L10.7795,10.509 "
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:13.5;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath15"
|
|
d="M10.1377,84.2715 C10.1377,75.9847 16.8555,69.2668 25.1423,69.2668 L25.7807,69.2668 C34.0675,69.2668 40.7853,75.9847 40.7853,84.2715 L40.1469,239.658 C40.1469,247.944 33.4291,254.663 25.1423,254.663 L24.504,254.663 C16.2172,254.663 9.4994,247.944 9.4994,239.658 L10.1377,84.2715 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:8;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
<g id="Layer6" name="carafe" opacity="1">
|
|
<path id="shapePath16"
|
|
d="M82.5,70.5 L281.5,70.5 L332.5,230.122 L305.207,230.122 L270.5,122.968 L253.5,122.967 L253.5,252.5 L106.5,252.5 L106.5,122.968 L82.5,70.5 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:11;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
<g id="Layer7" name="heating" opacity="1">
|
|
<path id="shapePath17" d="M154.14,171.887 C116.542,221.403 178.086,221.403 144.759,270.919 "
|
|
style="stroke:#000000;stroke-opacity:0.823529;stroke-width:20.5;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath18" d="M188.668,171.887 C151.071,221.403 212.613,221.403 179.286,270.919 "
|
|
style="stroke:#000000;stroke-opacity:0.823529;stroke-width:20.5;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath19" d="M221.858,171.887 C184.261,221.403 245.803,221.403 212.476,270.919 "
|
|
style="stroke:#000000;stroke-opacity:0.823529;stroke-width:20.5;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
<g id="Layer8" name="brewing" opacity="1">
|
|
<path id="shapePath20" d="M183.349,41.4504 L183.349,135.501 "
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:16;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;stroke-dasharray:48,32;fill:none;"/>
|
|
</g>
|
|
<g id="Layer9" name="ready" opacity="1">
|
|
<path id="shapePath21" d="M310.567,47.2444 L210.027,187.123 L166,146 "
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:23;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
<g id="Layer10" name="no-water" opacity="0">
|
|
<path id="shapePath22"
|
|
d="M122.5,155 C122.5,113.855 155.855,80.5 197,80.5 C238.145,80.5 271.5,113.855 271.5,155 C271.5,196.145 238.145,229.5 197,229.5 C155.855,229.5 122.5,196.145 122.5,155 Z"
|
|
style="stroke:#000000;stroke-opacity:0.47451;stroke-width:20;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath23" d="M147.412,195.101 C147.412,195.101 247.103,114.482 247.103,114.482 "
|
|
style="stroke:#000000;stroke-opacity:0.47451;stroke-width:20;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
<path id="shapePath24"
|
|
d="M196.525,98.5 L201.136,118.997 C204.122,137.531 227.896,146 227.896,172.094 C227.896,221.827 165.155,221.227 165.155,172.094 C165.155,145.791 187.942,137.596 192.007,118.997 L196.525,98.5 Z"
|
|
style="stroke:#000000;stroke-opacity:1;stroke-width:15;stroke-linejoin:miter;stroke-miterlimit:2;stroke-linecap:round;fill:none;"/>
|
|
</g>
|
|
</svg>
|
|
XML
|
|
|
|
sub SmarterCoffee_GetDevStateIcon {
|
|
my ($name, $colors) = @_;
|
|
|
|
my ($state, $icon) = (Value($name), $SmarterCoffee_StatusIconSVG);
|
|
|
|
my $noWater = (ReadingsVal($name, "water", "none") eq "none" and $state eq "maintenance");
|
|
my $waterLevel = ReadingsVal($name, "water_level", "0");
|
|
|
|
$state = "brewing" if ($state eq "grinding");
|
|
|
|
$icon =~ s/(name="ready" opacity)="1"/$1="0"/g if $state ne "ready";
|
|
$icon =~ s/(name="brewing" opacity)="1"/$1="0"/g if $state ne "brewing";
|
|
$icon =~ s/(name="coffee-level" opacity)="1"/$1="0"/g if ($state ne "brewing" and $state ne "done");
|
|
$icon =~ s/(name="(carafe|coffee-level)" opacity)="1"/$1="0"/g if (ReadingsVal($name, "carafe", "present") ne "present" or $noWater);
|
|
$icon =~ s/(name="heating" opacity)="1"/$1="0"/g if ReadingsVal($name, "hotplate", "off") ne "on";
|
|
|
|
$icon =~ s/(name="water-level.*" opacity)="1"/$1="0"/g;
|
|
$icon =~ s/(name="water-level-$waterLevel" opacity)="0"/$1="1"/;
|
|
$icon =~ s/(name="no-water" opacity)="0"/$1="1"/ if $noWater;
|
|
|
|
# Adjusting the icon color
|
|
my @stateColors = split(/\s+/, ($colors // ""));
|
|
for (my $i = 0; $i < int(@stateColors); $i++) {
|
|
$stateColors[$i] = undef if ($stateColors[$i] =~ /^[^#a-z0].*/i);
|
|
}
|
|
|
|
my %cm = (
|
|
"default" => ($stateColors[0] // "#7b7b7b"),
|
|
"ready" => ($stateColors[1] // "green"),
|
|
"brewing" => ($stateColors[2] // "chocolate"),
|
|
"done" => ($stateColors[3] // "#336699"),
|
|
);
|
|
|
|
if (my $stateColor = ($cm{$state} ? $cm{$state} : $cm{default})) {
|
|
$icon =~ s/(stroke|fill):#000000/$1:$stateColor/g;
|
|
}
|
|
|
|
# Removing any CR/LF to avoid wrapping in <pre> tags
|
|
$icon =~ s/[\r\n]//g;
|
|
|
|
return $icon;
|
|
}
|
|
|
|
sub SmarterCoffee_ReConnectTimer($) {
|
|
|
|
my $hash = shift;
|
|
|
|
|
|
if( defined($hash->{FD}) ) {
|
|
DevIo_Disconnected($hash);
|
|
DevIo_OpenDev($hash, 1, undef);
|
|
Log3 $hash->{NAME}, 4, "SmarterCoffee_ReConnectTimer - Socket Reconnected";
|
|
}
|
|
|
|
InternalTimer(gettimeofday() + 480, "SmarterCoffee_ReConnectTimer", $hash);
|
|
Log3 $hash->{NAME}, 4, "SmarterCoffee_ReConnectTimer - Call InternalTimer";
|
|
}
|
|
|
|
## -------------------------------------------------------------------------------------------------------------------
|
|
## Documentation follows
|
|
|
|
1;
|
|
|
|
=pod
|
|
=item device
|
|
=item summary Controls a Wi-Fi Smarter Coffee machine via network connection
|
|
=begin html
|
|
|
|
<a name="SmarterCoffee"></a>
|
|
<h3>SmarterCoffee</h3>
|
|
<ul>
|
|
Integrates the equally called Wi-Fi coffee machine (<code>http://smarter.am/</code>) with FHEM.
|
|
<br><br>
|
|
<i>Prerequisite</i>:<br>
|
|
Make sure the machine can be controlled by the smarter mobile app when both are connected to the same network as fhem.<br>
|
|
If in doubt check the official documentation or official support forum to get help with integrating the coffee machine into your network.
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="SmarterCoffeedefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> SmarterCoffee (<hostname>)</code>
|
|
<br><br>
|
|
Hostname is optional, if omitted the name is auto-detected via UDP broadcast.
|
|
<br><br>
|
|
Examples:<ul>
|
|
<li><code>define coffee-machine SmarterCoffee</code><br>
|
|
Connects with the first coffee machine that answers the UDP broadcast.</li><br>
|
|
<li><code>define coffee-machine SmarterCoffee smarter-coffee.fritz.box</code><br>
|
|
Connects with the coffee machine at address 'smarter-coffee.fritz.box'.</li><br>
|
|
<li><code>define coffee-machine SmarterCoffee 192.168.2.56:2081</code><br>
|
|
Connects with the coffee machine at '192.168.2.56' using port 2081 (= default)</li>
|
|
</ul>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="SmarterCoffeereadings"></a>
|
|
<b>Readings</b><br>
|
|
<ul>
|
|
<li>
|
|
<code>state</code><br>
|
|
Device state, can be one of:<ul>
|
|
<li><code>disconnected</code>: No connection to coffee machine.</li>
|
|
<li><code>opened / connected</code>: Intermediate states after connection has been established but before the machine's state is known.</li>
|
|
<li><code>invalid</code>: The connected device is not a coffee machine.</li>
|
|
<li><code>ready</code>: Ready to start brewing.</li>
|
|
<li><code>grinding</code>: Grinding coffee.</li>
|
|
<li><code>brewing</code>: Brewing coffee.</li>
|
|
<li><code>done</code>: Done brewing.</li>
|
|
<li><code>heating</code>: Keeping coffee warm or reheating.</li>
|
|
<li><code>maintenance</code>: Maintenance is needed to get ready for brewing (e.g. water or carafe is missing).</li>
|
|
</ul>
|
|
</li><br>
|
|
<li>
|
|
<code>hotplate_on_for_minutes</code><br>
|
|
Shows the number of minutes that the hotplate will be on when it was turned on via "<code>set <name> hotplate</code>"
|
|
or "<code>set <name> brew</code>".</li><br>
|
|
<li>
|
|
<code>carafe</code><br>
|
|
One of "<code>present</code>" or "<code>missing</code>" as the carafe is detected as being present or not.<br>
|
|
(<code>state</code> <code>ready</code> turns to <code>maintenance</code> when carafe is missing and <code>carafe_required</code> is <code>yes</code>)</li><br>
|
|
<li>
|
|
<code>carafe_required</code><br>
|
|
Is "<code>yes</code>" or "<code>no</code>" as the carafe is required to start brewing or not.<br>
|
|
This option can be configured via the smarter mobile app. Read disclaimer before turning off carafe detection.</li><br>
|
|
<li>
|
|
<code>cups_max</code><br>
|
|
The estimated maximum brewable cups when taking current water level into account.</li><br>
|
|
<li>
|
|
<code>cups_single_mode</code><br>
|
|
Is "<code>yes</code>" or "<code>no</code>" as the single cup mode is active or not.<br>
|
|
This option can be configured via the smarter mobile app.
|
|
When enabled "<code>set <name> brew</code>" will not enable the hotplate and "<code>cups_max</code>" is limited to <code>3</code>.
|
|
In single cup mode, cups [1,2,3] is used for one [small,medium,large] cup.</li><br>
|
|
<li>
|
|
<code>water</code> and <code>water_level</code><br>
|
|
Is [<code>none, low, half, full</code>] and [<code>0, 25, 50, 100</code>] indicating the amount of water that remains in the tank.<br>
|
|
(<code>state</code> <code>ready</code> turns to <code>maintenance</code> when <code>water_level</code> is "<code>0</code>")</li><br>
|
|
<li>
|
|
<code>last_command.*</code><br>
|
|
Is updated with the last executed <code>set</code> or <code>get</code> command string, including device response and success information.</li><br>
|
|
<li>
|
|
Further readings match <code>set</code> commands and reflect the corresponding machine state. See <i>Set</i> section below.</li><br>
|
|
</ul>
|
|
|
|
<a name="SmarterCoffeeget"></a>
|
|
<b>Get</b><br>
|
|
<ul>
|
|
<li>
|
|
<code>get <name> info</code><br>
|
|
Retrieves firmware & device type information and updates internals.</li><br>
|
|
<li>
|
|
<code>get <name> carafe_required_status</code><br>
|
|
Retrieves whether carafe is required for brewing and updates reading "carafe_required".</li><br>
|
|
<li>
|
|
<code>get <name> cups_single_mode_status</code><br>
|
|
Retrieves whether single cup mode is active and updates reading "cups_single_mode".</li><br>
|
|
<li>
|
|
<code>get <name> get_defaults</code><br>
|
|
Retrieves and applies previously set machine defaults. Triggers the event "defaults" after retrieval but before applying them.</li><br>
|
|
</ul>
|
|
|
|
<a name="SmarterCoffeeset"></a>
|
|
<b>Set</b><br>
|
|
<ul>
|
|
<li>
|
|
<code>set <name> brew</code><br>
|
|
Start brewing with settings displayed in readings and "<code>default-hotplate-on-for-minutes</code>" for hotplate.</li>
|
|
<li>
|
|
<code>set <name> brew current</code><br>
|
|
Start brewing with current machine settings.</li>
|
|
<li>
|
|
<code>set <name> brew [1 - 12] ([weak, medium, strong, extra]) ([5-40]) ([enabled, disabled])</code><br>
|
|
Start brewing the specified amount of cups at optionally specified strength, hotplate and grinder with non-specified settings used from readings
|
|
and "<code>default-hotplate-on-for-minutes</code>" for hotplate.
|
|
<p></p>
|
|
E.g. "<code>set <name> brew 5 medium</code>" brews 5 cups of medium coffee reusing current hotplate and grinder settings.</li><br>
|
|
<li>
|
|
<code>set <name> defaults ([1 - 12]) ([weak, medium, strong]) ([5-40]) ([enabled, disabled])</code><br>
|
|
Sets the machine defaults to the current settings optionally overridden by specified amount of cups, strength, hotplate and grinder.
|
|
Non-specified settings are used from readings and "<code>default-hotplate-on-for-minutes</code>" for hotplate.<br>
|
|
Note: Machine defaults are applied by the coffee machine after every brew and when stopping or turning off.
|
|
Readings are updated accordingly when defaults are applied.
|
|
<p></p>
|
|
E.g. "<code>set <name> defaults 5 medium</code>" sets defaults to 5 cups of medium coffee and reuses current hotplate and grinder settings
|
|
as future defaults.</li><br>
|
|
<li>
|
|
<code>set <name> stop</code><br>
|
|
Stop brewing and disable hotplate if on.</li><br>
|
|
<li>
|
|
<code>set <name> strength [weak, medium, strong, extra]</code><br>
|
|
Toggles the strength via the amount of coffee beans to use per cup when grinding.
|
|
<p></p>
|
|
The strength "<code>extra</code>" is special in that it is not natively supported by the machine itself.
|
|
To brew coffee with "extra" strength a custom sequence similar to the following is started:<br>
|
|
<code>set <name> brew <cups + 1> strong on enabled ;; sleep <cups * 1.5> ;; set <name> stop ;; set <name> brew <cups> on disabled</code>.<br>
|
|
See also attributes "<code>strength-extra-percent</code>" and "<code>strength-coffee-weights</code>" which control the calculation
|
|
of actual strength and cup counts.</li><br>
|
|
<li>
|
|
<code>set <name> grinder [enabled, disabled]</code><br>
|
|
Toggles whether grinder is used when brewing coffee.
|
|
Ground coffee has to be added manually to the filter and strength settings are ignored when grinder is disabled.</li><br>
|
|
<li>
|
|
<code>set <name> cups [1 - 12]</code><br>
|
|
Toggles the amount of cups (~100ml) to brew.</li><br>
|
|
<li>
|
|
<code>set <name> [1 - 12]-cups</code><br>
|
|
Is an alias to "<code>set <name> brew [1 - 12]</code>".
|
|
This adds support for web commands like "8-cups" and "3-cups,strong".</li><br>
|
|
<li>
|
|
<code>set <name> hotplate <command></code><br>
|
|
Toggles the hotplate that keeps the coffee warm after brewing.
|
|
<br><br>
|
|
<command> is one of:<ul>
|
|
<li><code>on</code><br>On for "<code>default-hotplate-on-for-minutes</code>" minutes (defaults to 15 minutes)</li>
|
|
<li><code>on [5 - 40]</code><br>On for the specified amount of minutes</li>
|
|
<li><code>off</code></li>
|
|
</ul>
|
|
</li><br>
|
|
<li>
|
|
<code>set <name> hotplate_on_for_minutes [5 - 40]</code><br>
|
|
Is an alias to "<code>set <name> hotplate on [5 - 40]</code>".</li><br>
|
|
<li>
|
|
<code>set <name> reconnect</code><br>
|
|
Disconnects, optionally runs discovery (if hostname or address was omitted in the device definition) and reconnects.</li><br>
|
|
<li>
|
|
<code>set <name> reset</code><br>
|
|
Resets machine to factory default, excluding WLAN settings.</li><br>
|
|
</ul>
|
|
|
|
<a name="SmarterCoffeeattr"></a>
|
|
<b>Attributes</b><br>
|
|
<ul>
|
|
<li>
|
|
<code>attr <name> devStateIcon { SmarterCoffee_GetDevStateIcon($name) }</code>
|
|
<br><br>
|
|
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 <name> 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".
|
|
Use '-' or '0' to substitute a color with the built-in or the default color within the color sequence. E.g. a color sequence of 'blue - 0 0' uses
|
|
blue for all states except "ready" which uses the built-in color green.</li><br>
|
|
<li>
|
|
<code>attr <name> default-hotplate-on-for-minutes 15</code><br>
|
|
Defines how long the hotplate is heating coffee when turning it on without specifying a time or when brewing coffee without
|
|
specifying a valid time value for hotplate.<br>
|
|
Values of 15, 5 or 40 minutes are used as this attribute is not specified, invalid or greater than 40 (5 <= x <= 40, with x defaulting to 15).
|
|
<br><br>
|
|
In addition to a fixed value, the hotplate can also be turned on relative to the number of cups that are brewed.
|
|
E.g. setting a value of "<code>15 5=20 10=35</code>" means: 15 from 1 to 4 cups, 20 from 5 cups and 35 from 10 cups.</li><br>
|
|
<li>
|
|
<code>attr <name> ignore-max-cups [1, 0]</code><br>
|
|
Toggles whether the reading "<code>cups_max</code>" influences the state "<code>ready</code>".
|
|
By default "<code>attr <name> ignore-max-cups 1</code>" is assumed which means that the state "<code>ready</code>" is not
|
|
related to "<code>cups_max</code>" if this attribute is not set. Set this attribute to "0" if the state should turn to "<code>maintenance</code>"
|
|
when the selected cup count is larger than "<code>cups_max</code>".</li><br>
|
|
<li>
|
|
<code>attr <name> set-on-brews-coffee [0, 1]</code><br>
|
|
Toggles whether the command "<code>set <name> on</code>" is an alias to "<code>set <name> brew</code>".
|
|
By default this is disabled to avoid accidental coffee brewing.</li><br>
|
|
<li>
|
|
<code>attr <name> strength-extra-percent 1.4</code><br>
|
|
Specifies the percentage of coffee to use relative to strength "<code>strong</code>" when brewing coffee with <code>extra</code> strength.
|
|
A value of "<code>1.4</code>" brews coffee that is 140% the strength of "<code>strong</code>" respectively "<code>0.6</code>" brews coffee that
|
|
is 60% the strength. Setting <code>strength-extra-percent</code> to <code>0</code> disables support for extra strength.
|
|
<br><br>
|
|
Note: Brewing coffee with <code>extra</code> strength uses strengths and cup counts natively supported by the machine and the configured percentage
|
|
is likely not matched exactly.
|
|
Best results are achieved with 4 - 8 cups and a full water tank as it allows to use most variations to get close to the target.</li><br>
|
|
<li>
|
|
<code>attr <name> strength-extra-pre-brew-delay-seconds 0</code><br>
|
|
Specifies a delay in seconds when brewing coffee with extra strength which is used to split the brewing operation in a pre-brew
|
|
and the normal brew phase. The pre-brew phase brews a small amount of cups (usually 1) and pauses for a couple of seconds
|
|
before continuing with the rest of the cups.
|
|
This mode can help to overcome limitations with grounds being too coarse to provide good taste at standard brewing speed.
|
|
Specifying 0 disables "pre-brew".
|
|
</li><br>
|
|
<li>
|
|
<code>attr <name> strength-extra-pre-brew-cups 1</code><br>
|
|
Specifies the number of cups that are brewed first before delaying brewing in extra mode. Specifying 0 disables "pre-brew".
|
|
</li><br>
|
|
<li>
|
|
<code>attr <name> strength-extra-start-on-device-strength [off, weak, medium, strong]</code><br>
|
|
Specifies a strength level that maps to strength 'extra' when starting brewing without grinder using the buttons at the coffee machine.
|
|
By default this option is set to "<code>off</code>" which means that strength 'extra' can only be used when starting brewing via FHEM.
|
|
<br>
|
|
E.g. a value of "<code>weak</code>" allows to brew coffee with extra strength by pressing the start button at the coffee machine
|
|
with strength set to "weak" and grinder set to "disabled" (= "Filter" in the display).
|
|
<br><br>
|
|
Note: Brewing started from FHEM is never affected by this setting.
|
|
</li><br>
|
|
<li>
|
|
<code>attr <name> strength-coffee-weights 3.5 3.9 4.3</code><br>
|
|
Is the amount of coffee that the grinder produces per cup depending on the selected strength. This setting does not control the amount it only
|
|
tells the module what the grinder will produce. Changing the default values is therefore only required if the coffee machine produces
|
|
different results on the actually used beans.<br>
|
|
The amounts are specified in grams per strength <code>[weak, medium, strong]</code> using whitespace as delimiter.
|
|
<br><br>
|
|
The purpose of this metric is to calculate the actual <code>strength</code> and <code>cups</code> to use when grinding coffee with
|
|
<code>extra</code> strength. E.g. for 140% extra strength, 4 cups require <tt style="white-space: nowrap">(4 * 4.3 * 1.4) = 24.08</tt>
|
|
gramms of coffee. In this example the closest match is grinding 7 cups with weak strength which produces <code>(7 * 3.5) = 24.5</code> gramms.
|
|
The actual brewing is then performed with 4 cups as originally requested.
|
|
<br><br>
|
|
The algorithm tries to find the closest matching cup counts and strength value towards the target amount of coffee required for
|
|
<code>extra</code> strength. It is technically not possible to control the amount of coffee directly, therefore the grams specified for the
|
|
different strengths are used select between natively supported strengths <code>weak, medium, strong</code> that match the desired target the closest.
|
|
Water level is also taken into account as cup count is truncated by the coffee machine when grinding, depending on the amount of available water.
|
|
Decisions may vary depending on cups and available water, keep water level at maximum to get best results.
|
|
<br><br>
|
|
Note: SCAE (Speciality Coffee Association of Europe) recommends ~6 gramms of coffee per cup. To come close, 140% is the default value for
|
|
<code>extra</code> strength assuming that the default values for <code>strength-coffee-weights</code> apply to the used coffee beans.
|
|
As the density of coffee beans differs these defaults may be inappropriate. To get better results with a certain kind of beans it may make
|
|
sense to measure the actual produced weights and adjust the values (<code>strength-coffee-weights</code> and <code>strength-extra-percent</code>)
|
|
accordingly.
|
|
</li><br>
|
|
</ul>
|
|
|
|
=end html
|
|
|
|
=cut
|