| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028 |
- #############################################################
- #
- # Copyright notice
- #
- # (c) 2016
- # Copyright: Juergen Kellerer (juergen at k123 dot eu)
- # FHEM Maintenance: Marko Oldenburg (leongaultier at gmail dot com)
- # All rights reserved
- #
- # This script free software; you can redistribute it and/or modify
- # 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: 98_SmarterCoffee.pm 17728 2018-11-11 11:01:55Z CoolTux $
- #
- #############################################################
- #
- # 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: 1.0.0
- #
- #############################################################
- # v1.0.0 - 2018-10-19
- # - change modul code to package module routine
- # - change code FHEM conform
- # - add multiple Attribut to control IoDev modul output
- # v0.9.1 - 2017-04-25
- # - fixed "stop" detection interferring with "extra" strength.
- # - added new state "grinding".
- # 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;
- my $version = "1.0.2";
- sub SmarterCoffee_Initialize($) {
- my ($hash) = @_;
- $hash->{DefFn} = 'SmarterCoffee::Define';
- $hash->{UndefFn} = 'SmarterCoffee::Undefine';
- $hash->{GetFn} = 'SmarterCoffee::Get';
- $hash->{SetFn} = 'SmarterCoffee::Set';
- $hash->{ReadFn} = 'SmarterCoffee::Read';
- $hash->{ReadyFn} = 'SmarterCoffee::OpenIfRequiredAndWritePending';
- $hash->{NotifyFn} = 'SmarterCoffee::Notify';
- $hash->{AttrFn} = 'SmarterCoffee::Attr';
- $hash->{AttrList} = ""
- . "default-hotplate-on-for-minutes "
- . "ignore-max-cups "
- . "set-on-brews-coffee "
- . "strength-coffee-weights "
- . "strength-extra-percent "
- . "strength-extra-pre-brew-cups "
- . "strength-extra-pre-brew-delay-seconds "
- . "strength-extra-start-on-device-strength:off,weak,medium,strong "
- . "devioLoglevel:0,1,2,3,4,5 "
- . $readingFnAttributes;
- foreach my $d ( sort keys %{ $modules{SmarterCoffee}{defptr} } ) {
- my $hash = $modules{SmarterCoffee}{defptr}{$d};
- $hash->{VERSION} = $version;
- }
- }
- package SmarterCoffee;
- use strict;
- use warnings;
- use POSIX;
- use GPUtils qw(:all)
- ; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
- use Data::Dumper;
- use Socket;
- use IO::Select;
- use DevIo;
- #use HttpUtils;
- ## Import der FHEM Funktionen
- BEGIN {
- GP_Import(
- qw(readingsSingleUpdate
- readingsBulkUpdate
- readingsBeginUpdate
- readingsEndUpdate
- CommandAttr
- defs
- modules
- Log3
- AttrVal
- ReadingsVal
- ReadingsNum
- Value
- IsDisabled
- deviceEvents
- init_done
- gettimeofday
- InternalTimer
- RemoveInternalTimer
- DoTrigger
- parseParams
- fhem)
- );
- }
- my $port = 2081;
- my $discoveryInterval = 60 * 15;
- my $strengthExtraDefaultPercent = 1.4;
- my $strengthDefaultWeights = "3.5 3.9 4.3";
- my %hotplate = ( default => 15, min => 5, max => 40 );
- my %messageMaps = (
- status_bitmasks => [
- # BIT 1 = ???
- # BIT 2 = hotplate
- # 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 %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 @getCommands = (
- "info", "carafe_required_status",
- "cups_single_mode_status", "get_defaults"
- ); #, "history"
- my %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 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 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 = ( $responseCodes{$1} // 0 ) ) {
- UpdateReadings(
- $hash,
- sub($) {
- my ($updateReading) = @_;
- while ( my ( $key, $value ) = each %{$response} ) {
- $updateReading->( "last_command_$key", $value );
- }
- $updateReading->(
- "last_command", $hash->{".last_set_command"}
- );
- },
- 1
- );
- }
- 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 );
- Log3 $hash->{NAME}, 5, 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 ),
- );
- ParseStatusValues( $hash, \%values );
- DoTrigger( $hash->{NAME}, "get_defaults" );
- Set( $hash, @{ [ $hash->{NAME}, "defaults" ] } );
- }
- # Parse carafe detection status message.
- if ( $message =~ /^4d([0-9a-f]{2})7e.*/ ) {
- UpdateReading( $hash, "carafe_required",
- ( $1 eq "01" ? "no" : "yes" ) );
- }
- # Parse single cup mode status message.
- if ( $message =~ /^50([0-9a-f]{2})7e.*/ ) {
- 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 ),
- );
- 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 DumpToExpression($) {
- my $d = Dumper( $_[0] );
- $d =~ s/\s+/ /g;
- $d =~ s/[^\}]*(\{.+\})[^\}]*/$1/;
- return $d;
- }
- sub 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 ( @{ $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: "
- . DumpToExpression($statusInfo);
- }
- }
- $values->{$mappingKey} = {%status};
- }
- else {
- if ( defined( $messageMaps{$mappingKey}{$rawValue} ) ) {
- $values->{$mappingKey} = $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);
- 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 Connect($) {
- my ($hash) = @_;
- my $isNewConnection =
- ReadingsVal( $hash->{NAME}, 'state', 'none' ) eq "initializing";
- readingsSingleUpdate( $hash, 'state', 'disconnected', 0 );
- delete $hash->{INVALID_DEVICE} if defined( $hash->{INVALID_DEVICE} );
- if ( $hash->{AUTO_DETECT} ) {
- RunDiscoveryProcess( $hash, 1 );
- }
- if ( defined( $hash->{DeviceName} ) ) {
- if ( not( $hash->{DeviceName} =~ m/^(.+):([0-9]+)$/ ) ) {
- $hash->{DeviceName} .= ":$port";
- }
- main::main::DevIo_CloseDev($hash) if main::DevIo_IsOpen($hash);
- delete $hash->{DevIoJustClosed} if ( $hash->{DevIoJustClosed} );
- return OpenIfRequiredAndWritePending( $hash, $isNewConnection );
- }
- return 0;
- }
- sub OpenIfRequiredAndWritePending($;$) {
- my ( $hash, $initial ) = @_;
- return main::DevIo_OpenDev( $hash, ( $initial ? 0 : 1 ),
- "SmarterCoffee::WritePending" );
- }
- sub HandleInitialConnectState($) {
- my ($hash) = @_;
- return if ( $hash->{".initial-connection-state"} );
- if (
- main::DevIo_IsOpen($hash)
- and ( ReadingsVal( $hash->{NAME}, 'state', 'none' ) eq "disconnected"
- or ReadingsVal( $hash->{NAME}, 'state', 'none' ) eq "opened" )
- )
- {
- $hash->{".initial-connection-state"} = 1;
- readingsSingleUpdate( $hash, 'state', 'connected', 0 );
- Get( $hash, @{ [ $hash->{NAME}, "info" ] } )
- if ( not $hash->{AUTO_DETECT} );
- Get( $hash, @{ [ $hash->{NAME}, "carafe_required_status" ] } );
- Get( $hash, @{ [ $hash->{NAME}, "cups_single_mode_status" ] } );
- delete $hash->{".initial-connection-state"};
- }
- }
- sub WritePending {
- my ( $hash, $mustSucceed ) = @_;
- if ( main::DevIo_IsOpen($hash) ) {
- my $pending = ( $hash->{PENDING_COMMAND} // 0 );
- # Handling initial call on a fresh connection
- HandleInitialConnectState($hash);
- # Processing pending commands
- if ( ( $hash->{INVALID_DEVICE} // "0" ) eq "1" ) {
- readingsSingleUpdate( $hash, 'state', 'invalid', 0 );
- }
- else {
- if ($pending) {
- delete $hash->{PENDING_COMMAND}
- if defined( $hash->{PENDING_COMMAND} );
- Log3 $hash->{NAME}, 4,
- "Connection :: Sending to "
- . $hash->{DeviceName}
- . ": $pending";
- main::DevIo_SimpleWrite( $hash, $pending, 1 );
- $hash->{".raw_last_status"} = "";
- my $result = main::DevIo_SimpleReadWithTimeout( $hash, 5 );
- if ($result) {
- $result = Read( $hash, $result );
- }
- else {
- main::DevIo_Disconnected($hash);
- }
- $hash->{INVALID_DEVICE} = "1"
- if ( $mustSucceed and not $result );
- $hash->{PENDING_COMMAND} = $pending if ( not $result );
- }
- }
- }
- return undef;
- }
- sub Read($;$) {
- my ( $hash, $buffer ) = @_;
- # Handle case that fhem reconnected a broken connection and state is "opened".
- 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 = main::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 ( ParseMessage($hash) ) {
- delete $hash->{PARTIAL};
- }
- else {
- Log3 $hash->{NAME}, 2,
- "Connection :: Failed parsing buffer content: "
- . $hash->{PARTIAL};
- return 0;
- }
- }
- return 1;
- }
- sub Define($$) {
- my ( $hash, $def ) = @_;
- my @param = split( '[ \t]+', $def );
- my $name = $hash->{NAME};
- # set default settings on first define
- if ($init_done) {
- CommandAttr( undef, $name . ' alias Coffee Machine' )
- if ( AttrVal( $name, 'alias', 'none' ) eq 'none' );
- CommandAttr( undef, $name . ' webCmd strength:cups:start:hotplate:off' )
- if ( AttrVal( $name, 'webCmd', 'none' ) eq 'none' );
- CommandAttr( undef,
- $name . ' strength-extra-percent ' . $strengthExtraDefaultPercent )
- if ( AttrVal( $name, 'strength-extra-percent', 'none' ) eq 'none' );
- CommandAttr( undef,
- $name . ' default-hotplate-on-for-minutes 15 5=20 8=30 10=35' )
- if ( AttrVal( $name, 'default-hotplate-on-for-minutes', 'none' ) eq
- 'none' );
- CommandAttr( undef, $name . ' event-on-change-reading .*' )
- if ( AttrVal( $name, 'event-on-change-reading', 'none' ) eq 'none' );
- CommandAttr( undef, $name . ' event-on-update-reading last_command.*' )
- if ( AttrVal( $name, 'event-on-update-reading', 'none' ) eq 'none' );
- CommandAttr( undef, $name . ' devioLoglevel 4' )
- if ( AttrVal( $name, 'devioLoglevel', 'none' ) eq 'none' );
- }
- CommandAttr( undef,
- $name . 'devStateIcon { SmarterCoffee::GetDevStateIcon($name) }' )
- if ( AttrVal( $name, 'devStateIcon', 'none' ) eq 'none'
- or AttrVal( $name, 'devStateIcon', 'none' ) eq
- '{ SmarterCoffee_GetDevStateIcon($name) }' );
- $hash->{VERSION} = $version;
- if ( int(@param) < 3 ) {
- $hash->{AUTO_DETECT} = 1;
- }
- else {
- delete $hash->{AUTO_DETECT};
- $hash->{DeviceName} = $param[2];
- }
- $hash->{NOTIFYDEV} = "global,$name";
- readingsSingleUpdate( $hash, 'state', 'initializing', 0 );
- $hash->{".last_command"} = $hash->{".last_response"} =
- $hash->{".last_status"} = $hash->{".raw_last_status"} = "";
- Connect($hash);
- $modules{SmarterCoffee}{defptr}{CoolTux} = $hash;
- Log3 $hash->{NAME}, 4,
- "Instance :: Defined module 'SmarterCoffee': " . Dumper($hash);
- }
- sub Undefine($$) {
- my ( $hash, $arg ) = @_;
- RemoveInternalTimer($hash);
- main::DevIo_CloseDev($hash);
- Log3 $hash->{NAME}, 4,
- "Instance :: Closed module 'SmarterCoffee': " . Dumper($hash);
- delete( $modules{SmarterCoffee}{defptr}{CoolTux} );
- return undef;
- }
- sub Attr(@) {
- my ( $cmd, $name, $attrName, $attrVal ) = @_;
- my $hash = $defs{$name};
- if ( $attrName eq "devioLoglevel" ) {
- if ( $cmd eq "set" ) {
- $hash->{devioLoglevel} = $attrVal;
- Log3 $name, 3,
- "SmarterCoffee ($name) - set devioLoglevel to $attrVal";
- }
- elsif ( $cmd eq "del" ) {
- delete $hash->{devioLoglevel};
- Log3 $name, 3,
- "SmarterCoffee ($name) - delete Internal devioLoglevel";
- }
- }
- return undef;
- }
- sub Get {
- my ( $hash, @param ) = @_;
- if ( grep { $_ eq ( $param[1] // "" ) } @getCommands ) {
- return Set( $hash, @param ) // "Ok :: " . $hash->{".last_response"};
- }
- else {
- return
- "Unknown argument $param[1], choose one of "
- . join( ":noArg ", @getCommands )
- . ":noArg";
- }
- }
- sub 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",
- $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 = $hotplate{max} if ( $value > $hotplate{max} );
- $value = $hotplate{min} if ( $value < $hotplate{min} );
- UpdateReading( $hash, "hotplate_on_for_minutes",
- ( $option eq "hotplate_off" ? 0 : $value ) );
- return unpack( 'H*', pack( 'C', $value ) );
- }
- elsif ( defined( $messageMaps{$option} ) and defined($optionValue) ) {
- # Ordinary values are looked up in the message maps (looking up the HEX code that backs a setting).
- for my $key ( keys %{ $messageMaps{$option} } ) {
- my $v = $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" )
- {
- Set( $hash, @{ [ $hash->{NAME}, "grinder", "enabled" ] } );
- $grinderEnabled = 1;
- $param[3] = "enabled" if defined( $param[3] );
- }
- if ( $option ne "defaults" and $grinderEnabled ) {
- if (
- TranslateParamsForExtraStrength( $hash, \@param, "grind" ) )
- {
- my ( $cups, $error ) = (
- $hash->{".extra_strength.desired_cups"},
- $hash->{".extra_strength.error_rate"}
- );
- Log3 $hash->{NAME}, 3,
- "Extra Strength :: Grinding ["
- . join( " ", @param )
- . "] to get $cups cups (error rate: $error%).";
- }
- else {
- 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};
- 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".
- 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" ) {
- ResetState($hash);
- }
- $messagePart = $optionToMessage->( $option, $param[0] );
- }
- # Command execution
- if ( defined( $commands{$option} ) ) {
- my $message = $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"} . "]";
- WritePending( $hash, ( $option eq "info" ) );
- }
- return undef;
- }
- elsif ( $option eq "disconnect" or $option eq "reconnect" ) {
- # This option is primarily to test if reconnect works.
- main::DevIo_Disconnected($hash);
- 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 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 ResetState($) {
- my ($hash) = @_;
- ResetBrewState($hash);
- ResetExtraStrengthMode($hash);
- }
- sub 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" ) {
- ReadConfiguration($hash)
- if ( grep( m/^(INITIALIZED|REREADCFG)$/, @{$events} ) );
- }
- else {
- for ( @{$events} ) {
- if ($_) {
- ProcessBrewStateEvents( $hash, $_ );
- ProcessEventForExtraStrength( $hash, $_ );
- LogCommands( $hash, $_ );
- }
- }
- }
- }
- }
- sub ReadConfiguration($) {
- my ($hash) = @_;
- # Restoring extra strength
- $hash->{".extra_strength.enabled"} = 1
- if ( ReadingsVal( $hash->{NAME}, "strength", "" ) =~ /^extra.*/ );
- }
- sub 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}, 3,
- "Command :: Failed [$command]; Cause: $message";
- }
- }
- }
- sub 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/ ) {
- 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.";
- ResetState($hash);
- }
- }
- sub ResetBrewState($) {
- my ($hash) = @_;
- delete $hash->{".brew-state"} if defined( $hash->{".brew-state"} );
- delete $hash->{"INITIATED_BREWING"}
- if defined( $hash->{"INITIATED_BREWING"} );
- }
- sub ProcessEventForExtraStrength($$) {
- my ( $hash, $event ) = @_;
- if ( $event =~ /^strength:\s*extra\s*$/ ) {
- # Listen to "set strength extra" and enable it if available.
- if ( not( EnableExtraStrengthMode($hash) ) ) {
- Log3 $hash->{NAME}, 3,
- "Extra-Strength :: Downgrading strength 'extra' to 'strong'";
- fhem( "sleep 0.1 fix-strength ; set "
- . $hash->{NAME}
- . " strength strong" );
- }
- }
- 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 EnableExtraStrengthMode($hash)
- )
- {
- Log3 $hash->{NAME}, 3,
- "Extra-Strength :: Upgrading brewing $cups cups started with disabled grinder and strength '$strength' to strength 'extra'.";
- Set( $hash, @{ [ $hash->{NAME}, "stop" ] } );
- 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
- )
- {
- Set(
- $hash,
- @{
- [
- $hash->{NAME}, "cups",
- $hash->{".extra_strength.original_desired_cups"}
- ]
- }
- );
- }
- 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"} =
- ExtraStrengthHandleBrewing($hash);
- }
- }
- }
- sub 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 ( TranslateParamsForExtraStrength( $hash, \@params, "brew" ) ) {
- # Resetting brew state to ensure it doesn't interfere with stop command that runs with "no-reset" option.
- ResetBrewState($hash);
- # Stopping brewing after initial grinding (skip stop if we are in phase-2 and came here due to pre-brew delay)
- 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 ) . "]";
- Set( $hash, @params );
- return 1;
- }
- return 0;
- }
- sub IsExtraStrengthModeAvailable($;$) {
- my ( $hash, $slient ) = @_;
- my $extraPercent = AttrVal( $hash->{NAME}, "strength-extra-percent",
- $strengthExtraDefaultPercent );
- my $preBrew =
- int( AttrVal( $hash->{NAME}, "strength-extra-pre-brew-cups", 1 ) ) *
- int(
- AttrVal( $hash->{NAME}, "strength-extra-pre-brew-delay-seconds", 0 ) );
- if ( $extraPercent > 0
- and ( $extraPercent != 1 or $preBrew > 0 )
- and $extraPercent < 2.5 )
- {
- 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 EnableExtraStrengthMode($) {
- my ($hash) = @_;
- return 1 if ( $hash->{".extra_strength.enabled"} );
- if ( IsExtraStrengthModeAvailable( $hash, 0 ) ) {
- Log3 $hash->{NAME}, 4,
- "Extra-Strength :: Entering extra strength mode.";
- $hash->{".extra_strength.enabled"} = 1;
- return 1;
- }
- else {
- return 0;
- }
- }
- sub 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 TranslateParamsForExtraStrength($$$) {
- my ( $hash, $params, $phase ) = @_;
- return 0 if ( not( EnableExtraStrengthMode($hash) ) );
- if ( $phase eq "grind" ) {
- my $extraPercent = AttrVal( $hash->{NAME}, "strength-extra-percent",
- $strengthExtraDefaultPercent );
- my @strengths = ( "weak", "medium", "strong" );
- my @weights = split(
- /\s+/,
- AttrVal(
- $hash->{NAME}, "strength-coffee-weights",
- $strengthDefaultWeights
- )
- );
- while ( int(@weights) < 3 ) {
- push( @weights,
- ( int(@weights) ? $weights[ int(@weights) - 1 ] : 4.3 ) );
- }
- Log3 $hash->{NAME}, 4,
- "Extra-Strength :: Reference weights: "
- . join( " ", @weights ) . " ("
- . join( " ", @strengths ) . ")";
- 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"};
- ResetExtraStrengthMode( $hash, 1 );
- }
- return 1;
- }
- return 0;
- }
- sub UpdateReading($$$) {
- my ( $hash, $name, $value ) = @_;
- UpdateReadings( $hash, sub($) { ( $_[0] )->( $name, $value ) } );
- }
- sub 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 ReadingsVal( $name, 'state', 'none' ) ne $value );
- };
- readingsBeginUpdate($hash);
- $callback->($updater);
- readingsEndUpdate( $hash, ( $updated or $forceUpdate ) );
- }
- sub RunDiscoveryProcess($;$) {
- my ( $hash, $skipConnect ) = @_;
- if ( Discover($hash) and not $skipConnect ) {
- Connect($hash);
- }
- InternalTimer(
- gettimeofday() + $discoveryInterval,
- "SmarterCoffee::RunDiscoveryProcess",
- $hash, 0
- );
- }
- sub InetSocketAddressString($) {
- my ( $sport, $inetAddress ) = sockaddr_in( $_[0] );
- return inet_ntoa($inetAddress) . ":$sport";
- }
- sub Discover($) {
- my ($hash) = @_;
- my $existingDeviceName = ( $hash->{DeviceName} // "" );
- my $broadcastAddress = sockaddr_in( $port, INADDR_BROADCAST );
- Log3 $hash->{NAME}, 4,
- "Discovery :: Broadcasting discovery request to "
- . InetSocketAddressString($broadcastAddress)
- . " (already discovered: $existingDeviceName)";
- socket( my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp') );
- setsockopt( $socket, SOL_SOCKET, SO_BROADCAST, 1 );
- 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 = InetSocketAddressString($deviceAddress);
- $message = unpack( 'H*', $message );
- Log3 $hash->{NAME}, 4,
- "Discovery :: Received message $message from $inetSocketAddress";
- if ( $message =~ /^65.*7e.*/ and ParseMessage( $hash, $message ) ) {
- my ( $sport, $inetAddress ) = sockaddr_in($deviceAddress);
- if ( my ($hostname) = gethostbyaddr( $inetAddress, AF_INET ) ) {
- $hash->{DeviceName} = $hostname . ":$sport";
- }
- 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 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;
- }
- ## -------------------------------------------------------------------------------------------------------------------
- ## 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
|