dolibarr 25.0.0-alpha
coffeemaker.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2026 Nick Fragoulis
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY, without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://gnu.org>.
16 */
17
24require_once DOL_DOCUMENT_ROOT . '/core/lib/geturl.lib.php';
25
32{
37 const COFFEE_TYPES = array(
38 'espresso', 'ristretto', 'cappuccino', 'latte', 'americano', 'macchiato', 'flat_white', 'mocha',
39 'nes', 'frappe', 'freddo_espresso', 'freddo_cappuccino', 'greek',
40 'cafe_au_lait', 'noisette', 'allonge',
41 'milchkaffee', 'eiskaffee', 'pharisaer',
42 'cortado', 'cafe_con_leche', 'carajillo', 'bombon'
43 );
44 const COFFEE_INTENSITIES = array('mild', 'normal', 'strong');
45
51 public function __construct(DoliDB $db)
52 {
53 $this->db = $db;
54 }
55
61 public function getDefinitions(): array
62 {
63 // Secret validation check to determine visibility
64 if (!getDolGlobalInt('AI_COFFEE_EASTER_EGG')) {
65 return []; // Hidden by default, behaves as non-existent
66 }
67
68 return [
69 [
70 "name" => "make_coffee",
71 "description" => "Prepares a cup of coffee using the local Home Assistant smart device framework.",
72 "inputSchema" => [
73 "type" => "object",
74 "properties" => [
75 "type" => [
76 "type" => "string",
77 "enum" => self::COFFEE_TYPES,
78 "description" => "The type of coffee to brew. Default is espresso. Greek: 'nes' (instant), 'frappe', 'freddo_espresso', 'freddo_cappuccino', 'greek' (ellinikos). French: 'cafe_au_lait', 'noisette' (espresso, dash of milk), 'allonge' (lungo). German: 'milchkaffee', 'eiskaffee' (with ice cream), 'pharisaer' (with rum and cream). Spanish: 'cortado', 'cafe_con_leche', 'carajillo' (with brandy), 'bombon' (condensed milk).",
79 "default" => "espresso"
80 ],
81 "intensity" => [
82 "type" => "string",
83 "enum" => self::COFFEE_INTENSITIES,
84 "description" => "The strength of the coffee flavor profile.",
85 "default" => "normal"
86 ]
87 ],
88 "required" => ["type"]
89 ]
90 ]
91 ];
92 }
93
100 public function getRequiredRights(string $toolName)
101 {
102 return array();
103 }
104
110 public function getCategories(): array
111 {
112 return ['smarthome', 'office'];
113 }
114
122 public function execute(string $name, array $args)
123 {
124 // Block execution if the easter egg is turned off
125 if (!getDolGlobalInt('AI_COFFEE_EASTER_EGG')) {
126 return ["error" => "Tool function '$name' not found."];
127 }
128
129 switch ($name) {
130 case 'make_coffee':
131 return $this->brewCoffee($args);
132 default:
133 return ["error" => "Tool function '$name' not found."];
134 }
135 }
136
143 private function brewCoffee(array $args): array
144 {
145 $type = $args['type'] ?? 'espresso';
146 $intensity = $args['intensity'] ?? 'normal';
147 // The enum constrains the model, not a direct caller: validate before
148 // anything reaches Home Assistant.
149 if (!in_array($type, self::COFFEE_TYPES, true)) {
150 return ["error" => "Unknown coffee type '".$type."'. Supported: ".implode(', ', self::COFFEE_TYPES)."."];
151 }
152 if (!in_array($intensity, self::COFFEE_INTENSITIES, true)) {
153 return ["error" => "Unknown intensity '".$intensity."'. Supported: ".implode(', ', self::COFFEE_INTENSITIES)."."];
154 }
155
156 $ha_url = getDolGlobalString('AI_COFFEE_HA_URL');
157 $token = getDolGlobalString('AI_COFFEE_HA_TOKEN');
158 $script_entity = getDolGlobalString('AI_COFFEE_HA_SCRIPT', 'script.brew_smart_coffee');
159
160 if (empty($ha_url) || empty($token)) {
161 return ["error" => "Home Assistant configuration is incomplete: set the constants AI_COFFEE_HA_URL and AI_COFFEE_HA_TOKEN (Home > Setup > Other), and optionally AI_COFFEE_HA_SCRIPT (default script.brew_smart_coffee)."];
162 }
163
164 // A long-lived HA token over plain http to a public host would cross the
165 // internet in cleartext. Refusing would break odd-but-legit setups, so
166 // warn instead.
167 $host = (string) parse_url($ha_url, PHP_URL_HOST);
168 $scheme = (string) parse_url($ha_url, PHP_URL_SCHEME);
169 if ($scheme == 'http' && $host != 'localhost' && !preg_match('/\.local$/', $host) && filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
170 dol_syslog("CoffeeMaker: AI_COFFEE_HA_URL uses plain http to a public host - the Home Assistant token is sent in cleartext. Use https (Nabu Casa or a TLS reverse proxy).", LOG_WARNING);
171 }
172
173 // Build proper HA API endpoint
174 $script_name = str_replace('script.', '', $script_entity);
175 $endpoint = rtrim($ha_url, '/') . '/api/services/script/' . $script_name;
176
177 $payload_data = [
178 'entity_id' => $script_entity,
179 'coffee_type' => $type,
180 'coffee_intensity' => $intensity
181 ];
182
183 $headers = [
184 "Authorization: Bearer " . $token,
185 "Content-Type: application/json"
186 ];
187
188 // localurl=2 (external AND local): Home Assistant almost always lives on
189 // the LAN (192.168.x / homeassistant.local) and getURLContent() refuses
190 // private/reserved ranges by default. Short timeouts: a chat turn must
191 // not hang on an unreachable coffee machine.
192 $result = getURLContent($endpoint, 'POST', json_encode($payload_data), 1, $headers, array('http', 'https'), 2, -1, 5, 10);
193
194 if ($result['curl_error_no'] != 0 || !in_array($result['http_code'], [200, 201])) {
195 dol_syslog("CoffeeMaker Error: " . (empty($result['curl_error_msg']) ? $result['http_code'] : $result['curl_error_msg']), LOG_ERR);
196 return ["error" => "Failed to communicate with coffee machine."];
197 }
198
199 return [
200 "status" => "success",
201 "info" => "Your {$intensity} {$type} is brewing!"
202 ];
203 }
204}
Class to manage Dolibarr database access.
Abstract base class for all MCP (Model Context Protocol) tools.
Class ToolCoffeeMaker.
__construct(DoliDB $db)
Constructor.
execute(string $name, array $args)
Executes the requested tool function based on its name.
brewCoffee(array $args)
Triggers the Home Assistant API dynamically using stored configuration.
getDefinitions()
Returns tool definitions if the hidden constant is enabled.
getCategories()
Return categories this tool belongs to.
getRequiredRights(string $toolName)
No business data.
const COFFEE_TYPES
Beverages the bridge accepts; the Home Assistant script receives the value verbatim as coffee_type an...
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).