├── doc.json ├── homeassistant ├── README.md ├── card-example.png ├── decent-card.yaml └── rest-sensor-switch.yaml ├── homebridge_http-switch_config.json ├── index.html ├── plugin.tcl ├── readme.md └── settings.tdb /doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": [ 3 | { 4 | "method": "GET", 5 | "path": "/api/status", 6 | "description": "running status of the machine" 7 | }, 8 | { 9 | "method": "POST", 10 | "path": "/api/status/ {\"active\": \"true/false\"}", 11 | "description": "change the status of the machine - on or standby" 12 | }, 13 | { 14 | "method": "GET", 15 | "path": "/api/status/details", 16 | "description": "get a detailed status of your machine - the available information depends on the state/substate" 17 | }, 18 | { 19 | "method": "GET", 20 | "path": "/api/shot/", 21 | "description": "get a list of the shots on your machine" 22 | }, 23 | { 24 | "method": "GET", 25 | "path": "/api/shot/{shot.tcl}", 26 | "description": "get a single shot as " 27 | }, 28 | { 29 | "method": "GET", 30 | "path": "/api/profile", 31 | "description": "list of profiles on your machine" 32 | }, 33 | { 34 | "method": "GET", 35 | "path": "/api/profile/{profile_filename.tcl}", 36 | "description": "" 37 | }, 38 | { 39 | "method": "POST", 40 | "path": "/api/profile/", 41 | "description": "add a profile to your machine - the filename of the uploaded profile will be used, so existing profiles could be overwritten" 42 | }, 43 | { 44 | "method": "PUT", 45 | "path": "/api/profile/{profile_filename.tcl}", 46 | "description": "Select an existing profile" 47 | }, 48 | { 49 | "method": "GET", 50 | "path": "/", 51 | "description": "Dashboard including most API functions" 52 | }, 53 | { 54 | "method": "GET", 55 | "path": "/api/flush", 56 | "description": "Flush the log" 57 | }, 58 | { 59 | "method": "PUT", 60 | "path": "/api/v2/shot/{timestamp}", 61 | "description": "Set the specified shots' DYE description as next planned shot in DYE." 62 | }, 63 | { 64 | "method": "GET", 65 | "path": "/api/v2/shot/{timestamp}", 66 | "description": "Returns a json representation of the specified shot (if found)." 67 | }, 68 | { 69 | "method": "GET", 70 | "path": "/api/v2/shots", 71 | "description": "Returns a json array of all the shots data present in SDB. Does not include graph data." 72 | }, 73 | { 74 | "method": "GET", 75 | "path": "/api/help", 76 | "description": "Get this short API doc." 77 | } 78 | ], 79 | "authentication": "Authentication is done with a header param auth - if enabled it needs to match the configured string" 80 | } -------------------------------------------------------------------------------- /homeassistant/README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Example Configuration 2 | 3 | This directory contains an example configuration for how you can integrate the Rest API with Home Assistant. 4 | 5 | ## Configuration 6 | 7 | The file [`rest-sensor-switch.yaml`](rest-sensor-switch.yaml) contains the a REST sensor / switch configuration that you can add 8 | to your Home Assistant's `configuration.yaml` file (you need to restart HA for the changes to be picked up). 9 | Make sure to replace the two occurances of `` with the IP address of your Decent Machine's Tablet. 10 | 11 | ## Dasboard 12 | 13 | An example dashboard (lovelace) card stack can be found in [`decent-card.yaml`](decent-card.yaml). 14 | To use it do the following: 15 | - Go into edit mode on your dashboard 16 | - Click "Add Card" 17 | - Select "Manual" (at the bottom) 18 | - Paste the code from the file 19 | 20 | 21 | ![](card-example.png) 22 | -------------------------------------------------------------------------------- /homeassistant/card-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randomcoffeesnob/decent-advanced-rest-api/f6752e674be08eeb4f970b4122f661ab0c605151/homeassistant/card-example.png -------------------------------------------------------------------------------- /homeassistant/decent-card.yaml: -------------------------------------------------------------------------------- 1 | # This is an example of a lovelace card stack to display the values 2 | # gathered via the rest sensor. 3 | # To use this, go into edit mode on your dashboard, 4 | # click "Add Card", select "Manual" (at the bottom) and paste the code below 5 | 6 | type: vertical-stack 7 | cards: 8 | - type: grid 9 | columns: 2 10 | square: false 11 | cards: 12 | - type: tile 13 | entity: switch.decent_espresso 14 | icon: mdi:coffee-maker-outline 15 | - type: tile 16 | entity: sensor.decent_espresso_profile 17 | name: Profile 18 | icon: mdi:progress-wrench 19 | - square: false 20 | columns: 2 21 | type: grid 22 | cards: 23 | - type: tile 24 | entity: sensor.decent_espresso_state 25 | name: State 26 | icon: mdi:coffee-maker-outline 27 | - type: tile 28 | entity: sensor.decent_espresso_substate 29 | name: Substate 30 | icon: mdi:list-status 31 | - square: false 32 | columns: 4 33 | type: grid 34 | cards: 35 | - type: gauge 36 | entity: sensor.decent_espresso_head_temperature 37 | name: Head 38 | max: 105 39 | - type: gauge 40 | entity: sensor.decent_espresso_mix_temperature 41 | name: Mix 42 | max: 105 43 | - type: gauge 44 | entity: sensor.decent_espresso_steam_temperature 45 | max: 175 46 | name: Steam 47 | - type: gauge 48 | needle: true 49 | entity: sensor.decent_espresso_water_level 50 | name: Water 51 | max: 1500 52 | unit: ml 53 | segments: 54 | - from: 0 55 | color: var(--error-color) 56 | - from: 200 57 | color: var(--warning-color) 58 | - from: 500 59 | color: var(--success-color) 60 | - square: false 61 | columns: 2 62 | type: grid 63 | cards: 64 | - hours_to_show: 24 65 | graph: line 66 | type: sensor 67 | detail: 1 68 | entity: sensor.decent_espresso_shot_count 69 | name: Shot Count 70 | icon: mdi:coffee-outline 71 | - hours_to_show: 24 72 | graph: line 73 | type: sensor 74 | detail: 1 75 | entity: sensor.decent_espresso_steam_count 76 | name: Steam Count 77 | icon: mdi:kettle-steam-outline 78 | -------------------------------------------------------------------------------- /homeassistant/rest-sensor-switch.yaml: -------------------------------------------------------------------------------- 1 | # Put this into your homeassistant configuration.yaml file 2 | # Replace both instances with the IP address of the tablet on your Decent machine 3 | 4 | rest: 5 | # when using webserver_authentication 1 6 | #- resource: http://:8888/api/status/details?auth=myFancyAuthenticationKey 7 | - resource: http://:8888/api/status/details 8 | scan_interval: 2 9 | sensor: 10 | - name: Decent Espresso State 11 | unique_id: decentespresso_state 12 | value_template: "{{ value_json.state }}" 13 | - name: Decent Espresso Substate 14 | unique_id: decentespresso_substate 15 | value_template: "{{ value_json.substate }}" 16 | - name: Decent Espresso Shot Count 17 | unique_id: decentespresso_espresso_count 18 | value_template: "{{ value_json.espresso_count|is_defined }}" 19 | - name: Decent Espresso Steam Count 20 | unique_id: decentespresso_steaming_count 21 | value_template: "{{ value_json.steaming_count|is_defined }}" 22 | - name: Decent Espresso Head Temperature 23 | unique_id: decentespresso_head_temp 24 | value_template: "{{ value_json.head_temperature|default(0)|round(1) }}" 25 | unit_of_measurement: "ºC" 26 | - name: Decent Espresso Mix Temperature 27 | unique_id: decentespresso_mix_temp 28 | value_template: "{{ value_json.mix_temperature|default(0)|round(1) }}" 29 | unit_of_measurement: "ºC" 30 | - name: Decent Espresso Steam Temperature 31 | unique_id: decentespresso_steam_temp 32 | value_template: "{{ value_json.steam_heater_temperature|default(0)|round(1) }}" 33 | unit_of_measurement: "ºC" 34 | - name: Decent Espresso Water Level 35 | unique_id: decentespresso_water_level 36 | value_template: "{{ value_json.water_level_ml|default(0) }}" 37 | unit_of_measurement: ml 38 | - name: Decent Espresso Profile 39 | unique_id: decentespresso_profile 40 | value_template: "{{ value_json.profile|replace('_', ' ') }}" 41 | 42 | switch: 43 | - platform: rest 44 | name: "Decent Espresso" 45 | resource: http://:8888/api/status 46 | # when using webserver_authentication 1 47 | #resource: http://:8888/api/status?auth=myFancyAuthenticationKey 48 | body_on: '{"active": "true"}' 49 | body_off: '{"active": "false"}' 50 | is_on_template: "{{ value_json.is_active }}" 51 | headers: 52 | Content-Type: application/json 53 | -------------------------------------------------------------------------------- /homebridge_http-switch_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessories": [ 3 | { 4 | "accessory": "HTTP-SWITCH", 5 | "name": "Decent DE1", 6 | "switchType": "stateful", 7 | "onUrl": { 8 | "url": "http://DECENT-IP:8888/api/status", 9 | "method": "POST", 10 | "body": "{\"active\": \"true\"}", 11 | "headers": { 12 | "Content-Type": "application/json" 13 | } 14 | }, 15 | "offUrl": { 16 | "url": "http://DECENT-IP:8888/api/status", 17 | "method": "POST", 18 | "body": "{\"active\": \"false\"}", 19 | "headers": { 20 | "Content-Type": "application/json" 21 | } 22 | }, 23 | "statusUrl": { 24 | "url": "http://DECENT-IP:8888/api/status", 25 | "statusPattern": "{\"is_active\":\"true\"(,\"espresso_count\":[0-9]+,\"steaming_count\":[0-9]+})?", 26 | "method": "GET" 27 | } 28 | 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 28 | 472 | 473 | 474 | 475 |
476 | 477 | 478 | 481 |
482 | 483 |
484 | 485 | 486 |
487 | 488 | 489 | 490 | 491 |
492 |
493 |
494 |
495 | 496 | 497 | 498 |
499 | 500 | 501 | 514 | 515 | 516 | 517 |
518 |
519 |

520 |

521 | 522 |
523 |
524 | 525 | 526 |
527 |
528 |
529 |

Installed Profiles

530 | 531 | 544 | 545 | 546 |
547 |

548 | 549 |
550 |
551 |
552 |
553 |

Upload Profiles

554 |
555 | 556 |
557 | 559 | 560 | 561 |
562 |
563 |
564 |
565 | 566 | 567 | 568 |
569 |
    570 | 571 |
    572 | 573 | 574 | 575 |
    576 | 577 |
    578 |
    579 | 580 |

    Info

    581 | 582 |
    583 |
    584 | 585 | 586 | 587 | -------------------------------------------------------------------------------- /plugin.tcl: -------------------------------------------------------------------------------- 1 | package require de1_machine 1.2 2 | package require json 3 | package require de1_profile 2.0 4 | package require de1_vars 1.0 5 | package require de1_utils 1.1 6 | 7 | set plugin_name "advanced_rest_api" 8 | 9 | namespace eval ::plugins::${plugin_name} { 10 | 11 | variable author "Yannick Dietler" 12 | variable contact "ydt@ydt.ch" 13 | variable version 1.3 14 | variable description "API to control the DE1's power state and getting additional Information" 15 | variable name "Advanced REST API" 16 | 17 | # based on Johanna Schander's Web API 18 | proc main {} { 19 | package require wibble 20 | 21 | # Create settings if non-existant 22 | if {[array size ::plugins::advanced_rest_api::settings] == 0} { 23 | array set ::plugins::advanced_rest_api::settings { 24 | webserver_port 8888 25 | webserver_authentication_key "myFancyAuthenticationKey" 26 | } 27 | save_plugin_settings advanced_rest_api 28 | } 29 | 30 | 31 | 32 | # Auth 33 | 34 | proc ::wibble::check_auth {state} { 35 | set auth [dict getnull $state request query auth] 36 | set auth [lindex $auth 1] 37 | 38 | if {$auth eq "" && $::plugins::advanced_rest_api::settings(webserver_authentication) == 1} { 39 | return [unauthorized $state] 40 | } 41 | 42 | if {$auth != $::plugins::advanced_rest_api::settings(webserver_authentication_key) && $::plugins::advanced_rest_api::settings(webserver_authentication) == 1} { 43 | return [unauthorized $state] 44 | } 45 | 46 | return true; 47 | } 48 | 49 | proc ::wibble::unauthorized {state} { 50 | dict set response status 403 51 | dict set state response header content-type "" {application/json charset utf-8} 52 | dict set response content "{status: \"unauthorized\"}" 53 | sendresponse $response 54 | return false; 55 | } 56 | 57 | # Utilities 58 | 59 | proc ::wibble::return_200_json {content} { 60 | dict set response status 200 61 | dict set response header content-type {} application/json 62 | dict set response content "$content\n" 63 | sendresponse $response 64 | } 65 | # from https://wiki.tcl-lang.org/page/JSON 66 | proc ::wibble::compile_json {spec data} { 67 | while {[llength $spec]} { 68 | set type [lindex $spec 0] 69 | set spec [lrange $spec 1 end] 70 | 71 | switch -- $type { 72 | dict { 73 | lappend spec * string 74 | 75 | set json {} 76 | foreach {key val} $data { 77 | foreach {keymatch valtype} $spec { 78 | if {[string match $keymatch $key]} { 79 | lappend json [subst {"$key":[ 80 | ::wibble::compile_json $valtype $val]}] 81 | break 82 | } 83 | } 84 | } 85 | return "{[join $json ,]}" 86 | } 87 | list { 88 | if {![llength $spec]} { 89 | set spec string 90 | } else { 91 | set spec [lindex $spec 0] 92 | } 93 | set json {} 94 | foreach {val} $data { 95 | lappend json [::wibble::compile_json $spec $val] 96 | } 97 | return "\[[join $json ,]\]" 98 | } 99 | string { 100 | if {[string is double -strict $data]} { 101 | return $data 102 | } else { 103 | return "\"[::wibble::escape_json $data]\"" 104 | } 105 | } 106 | default {error "Invalid type"} 107 | } 108 | } 109 | } 110 | 111 | proc ::wibble::escape_json {input_str} { 112 | # Replace all newlines (\n) in the string with an escaped newline string 113 | set output_str $input_str 114 | 115 | # Escape backslashes 116 | regsub -all {\\} $output_str {\\\\} output_str 117 | 118 | # Escape double quotes 119 | regsub -all {"} $output_str {\\"} output_str 120 | 121 | # Escape forward slashes 122 | regsub -all {/} $output_str {\\/} output_str 123 | 124 | # Escape special control characters 125 | regsub -all {\n} $output_str {\\n} output_str 126 | regsub -all {\r} $output_str {\\r} output_str 127 | regsub -all {\t} $output_str {\\t} output_str 128 | regsub -all {\b} $output_str {\\b} output_str 129 | regsub -all {\f} $output_str {\\f} output_str 130 | 131 | # Return the escaped string 132 | return $output_str 133 | } 134 | # Index endpoint 135 | proc ::wibble::indexpage {state} { 136 | if { ![check_auth $state] } { 137 | return; 138 | } 139 | set fp [open "[homedir]/[plugin_directory]/advanced_rest_api/index.html" r] 140 | set file_data [read $fp] 141 | close $fp 142 | 143 | dict set state response status 200 144 | dict set state response header content-type "" text/html 145 | dict set state response content $file_data 146 | sendresponse [dict get $state response] 147 | } 148 | 149 | # Profile endpoints 150 | 151 | proc ::wibble::profile {state } { 152 | if { ![check_auth $state] } { 153 | return; 154 | } 155 | set method [dict get $state request method] 156 | if {$method eq "POST"} { 157 | set rawheaders [dict get $state request rawheader] 158 | set filenameIndex [lsearch $rawheaders "filename:*"] 159 | if {$filenameIndex == -1 } { 160 | set localfilename "[clock seconds].tcl" 161 | } else { 162 | set localfilename [lindex [split [lindex $rawheaders $filenameIndex] ": "] end] 163 | } 164 | set postdata [dict get $state request rawpost] 165 | set path "[pwd]/profiles/$localfilename" 166 | set fileId [open $path "w"] 167 | puts -nonewline $fileId $postdata 168 | close $fileId 169 | ::wibble::return_200_json "$localfilename written" 170 | } 171 | if {$method eq "GET"} { 172 | set path [dict get $state request path] 173 | set profile [lindex [split $path "/"] 3] 174 | #Load all saved profiles as a list 175 | set savedprofiles [glob -tails -directory [pwd]/profiles/ *.tcl] 176 | 177 | if {$profile != ""} { 178 | #Only return profile information if the profile exists 179 | if {$profile in $savedprofiles} { 180 | set fd [open "[pwd]/profiles/$profile" r] 181 | fconfigure $fd -translation binary 182 | set content [read $fd]; close $fd 183 | # ::wibble::return_200_json [::wibble::compile_json {dict} $content] 184 | ::wibble::return_200_json $content 185 | } else { 186 | #If a profile is specified but does not exist, return all profiles 187 | ::wibble::return_200_json [::wibble::compile_json {list} $savedprofiles] 188 | } 189 | } else { 190 | #If no profile was specified, return all profiles 191 | ::wibble::return_200_json [::wibble::compile_json {list} $savedprofiles] 192 | } 193 | } 194 | #Set a profile 195 | if {$method eq "PUT"} { 196 | set path [dict get $state request path] 197 | set profile [lindex [split $path "/"] 3] 198 | #Load all saved profiles as a list 199 | set savedprofiles [glob -tails -directory [pwd]/profiles/ *.tcl] 200 | #Only set the profile if it exists 201 | if {$profile in $savedprofiles} { 202 | #The select_profile procedure accepts the profile name without file extension (.tcl) 203 | set rootname [file rootname [file tail $profile]] 204 | select_profile $rootname 205 | ::wibble::return_200_json "$profile" 206 | } else { 207 | ::wibble::return_200_json [::wibble::compile_json {list} $savedprofiles] 208 | } 209 | } 210 | 211 | } 212 | 213 | 214 | #history 215 | proc ::wibble::history {state} { 216 | if { ![check_auth $state] } { 217 | return; 218 | } 219 | set path [dict get $state request path] 220 | set shot [lindex [split $path "/"] 3] 221 | 222 | 223 | if {$shot != ""} { 224 | set fd [open "[pwd]/history/$shot" r] 225 | fconfigure $fd -translation binary 226 | set content [read $fd]; close $fd 227 | ::wibble::return_200_json $content 228 | } else { 229 | set shotlist [glob -tails -directory [pwd]/history/ *.shot] 230 | ::wibble::return_200_json [::wibble::compile_json "{list}" $shotlist] 231 | } 232 | } 233 | 234 | #history v2 235 | proc ::wibble::history_v2 {state} { 236 | if { ![check_auth $state] } { 237 | return; 238 | } 239 | set method [dict get $state request method] 240 | if { $method eq "PUT" } { 241 | return [set_next_shot_in_dye $state] 242 | } 243 | set path [dict get $state request path] 244 | set shot [lindex [split $path "/"] 4] 245 | append shotName [lindex [split $shot "."] 0] ".json" 246 | 247 | if {$shotName != ""} { 248 | set fd [open "[pwd]/history_v2/$shotName" r] 249 | fconfigure $fd -translation binary 250 | set content [read $fd]; close $fd 251 | ::wibble::return_200_json $content 252 | } else { 253 | ::wibble::return_200_json 254 | } 255 | } 256 | 257 | proc ::wibble::set_next_shot_in_dye { state } { 258 | set path [dict get $state request path] 259 | set shot [lindex [split $path "/"] 4] 260 | 261 | msg -INFO "shot name is ${shot}" 262 | 263 | set fields { 264 | workflow_settings 265 | shot_profile 266 | ratio 267 | drink_weight 268 | grinder_dose_weight 269 | grinder_setting 270 | grinder_model 271 | workflow 272 | bean_brand 273 | bean_type 274 | roast_date 275 | roast_level 276 | bean_notes 277 | } 278 | ::plugins::DYE::shots::source_next_from $shot {} $fields 279 | ::wibble::return_200_json "" 280 | } 281 | 282 | proc ::wibble::history_sdb { state } { 283 | if { ![check_auth $state] } { 284 | return; 285 | } 286 | # TODO: check SDB plugin exists and is loaded 287 | array set loadedShots [::plugins::SDB::shots *] 288 | set jsonArray {} 289 | set listLength [llength $loadedShots(grinder_setting)] 290 | 291 | for {set i 0} {$i < $listLength} {incr i} { 292 | # Create a dictionary for each index 293 | set shotDict [dict create \ 294 | clock [lindex $loadedShots(clock) $i] \ 295 | grinder_setting [lindex $loadedShots(grinder_setting) $i] \ 296 | grinder_model [lindex $loadedShots(grinder_model) $i] \ 297 | profile_title [lindex $loadedShots(profile_title) $i] \ 298 | bean_desc [lindex $loadedShots(bean_desc) $i] \ 299 | bean_brand [lindex $loadedShots(bean_brand) $i] \ 300 | bean_type [lindex $loadedShots(bean_type) $i] \ 301 | bean_notes [string map {\n \\n} [lindex $loadedShots(bean_notes) $i]] \ 302 | drink_weight [lindex $loadedShots(drink_weight) $i] \ 303 | grinder_dose_weight [lindex $loadedShots(grinder_dose_weight) $i] \ 304 | target_drink_weight [lindex $loadedShots(target_drink_weight) $i] \ 305 | extraction_time [lindex $loadedShots(extraction_time) $i] \ 306 | filename [lindex $loadedShots(filename) $i] \ 307 | espresso_enjoyment [lindex $loadedShots(espresso_enjoyment) $i] \ 308 | espresso_notes [string map {\n \\n} [lindex $loadedShots(espresso_notes) $i]] \ 309 | shot_desc [lindex $loadedShots(shot_desc) $i]] 310 | 311 | # Append the dictionary to the jsonArray list 312 | lappend jsonArray $shotDict 313 | } 314 | ::wibble::return_200_json [::wibble::compile_json {list dict} $jsonArray] 315 | } 316 | 317 | # based on https://github.com/Testsubject1683/de1-mirror/tree/webapi 318 | proc ::wibble::status {} { 319 | 320 | # depending on the current state, we supply different type of data 321 | set return [dict create] 322 | set json_structure {dict state string} 323 | dict set return "state" $::de1_num_state($::de1(state)) 324 | dict set return "substate" $::de1_substate_types($::de1(substate)) 325 | dict set return "battery_percent" [battery_percent] 326 | dict set return "charger_on" $::de1(usb_charger_on) 327 | 328 | switch -- $::de1_num_state($::de1(state)) { 329 | "Idle" { 330 | dict set return "profile" [::profile::filename_from_title $::settings(profile_title)] 331 | dict set return "espresso_count" $::settings(espresso_count) 332 | dict set return "steaming_count" $::settings(steaming_count) 333 | dict set return "bean_brand" $::settings(bean_brand) 334 | dict set return "bean_type" $::settings(bean_type) 335 | dict set return "bean_notes" $::settings(bean_notes) 336 | dict set return "roast_date" $::settings(roast_date) 337 | dict set return "roast_level" $::settings(roast_level) 338 | dict set return "skin" [::profile::filename_from_title $::settings(skin)] 339 | dict set return "head_temperature" [expr [expr {floor([expr $::de1(head_temperature) * 100])} / 100]] 340 | dict set return "mix_temperature" [expr [expr {floor([expr $::de1(mix_temperature) * 100])} / 100]] 341 | dict set return "steam_heater_temperature" [expr [expr {floor([expr $::de1(steam_heater_temperature) * 100])} / 100]] 342 | dict set return "water_level_ml" [water_tank_level_to_milliliters $::de1(water_level)] 343 | } 344 | "Espresso" { 345 | foreach key [list "espresso_elapsed" "espresso_pressure" "espresso_weight" "espresso_flow" "espresso_flow_weight" "espresso_temperature_basket" "espresso_temperature_mix"] { 346 | #dict append ret "$key" [::${key} range 0 end] 347 | append json_structure " ${key} list" 348 | dict set return $key [split [::${key} range 0 end] " "] 349 | } 350 | } 351 | "Sleep" { 352 | } 353 | "GoingToSleep" { 354 | } 355 | "Busy" { 356 | } 357 | "Steam" { 358 | } 359 | "HotWater" { 360 | } 361 | "ShortCal" { 362 | } 363 | "SelfTest" { 364 | } 365 | "LongCal" { 366 | } 367 | "Descale" { 368 | } 369 | "FatalError" { 370 | } 371 | "Init" { 372 | } 373 | "NoRequest" { 374 | } 375 | "SkipToNext" { 376 | } 377 | "HotWaterRinse" { 378 | } 379 | "SteamRinse" { 380 | } 381 | "Refill" { 382 | } 383 | "Clean" { 384 | } 385 | "InBootLoader" { 386 | } 387 | "AirPurge" { 388 | } 389 | } 390 | append json_structure " * string" 391 | ::wibble::return_200_json [::wibble::compile_json $json_structure $return] 392 | } 393 | proc ::wibble::docs {state} { 394 | set fp [open "[homedir]/[plugin_directory]/advanced_rest_api/doc.json" r] 395 | set file_data [read $fp] 396 | close $fp 397 | 398 | 399 | ::wibble::return_200_json $file_data 400 | } 401 | 402 | proc ::wibble::state {state} { 403 | if { ![check_auth $state] } { 404 | return; 405 | } 406 | set method [dict get $state request method] 407 | set current_state $::de1_num_state($::de1(state)) 408 | 409 | if {$method eq "GET"} { 410 | set path [dict get $state request path] 411 | 412 | switch -- $path { 413 | "/api/status/details" { 414 | ::wibble::status 415 | } 416 | "/api/status" { 417 | if { $::de1_num_state($::de1(state)) != "Sleep" } { 418 | dict set state_response is_active true 419 | dict set state_response espresso_count $::settings(espresso_count) 420 | dict set state_response steaming_count $::settings(steaming_count) 421 | } else { 422 | dict set state_response is_active false 423 | } 424 | 425 | ::wibble::return_200_json [::wibble::compile_json {dict} $state_response] 426 | } 427 | } 428 | } 429 | if {$method eq "POST"} { 430 | set postdata [::json::json2dict [dict get $state request rawpost]] 431 | if {[dict exists $postdata active]} { 432 | set new_state [dict get $postdata active] 433 | switch -- $new_state { 434 | "false" { 435 | if {$current_state != "Sleep"} { 436 | start_sleep 437 | } 438 | dict set state_change_response is_active false 439 | ::wibble::return_200_json [::wibble::compile_json {dict} $state_change_response] 440 | 441 | } 442 | "true" { 443 | if {$current_state != "Idle"} { 444 | start_idle 445 | } 446 | dict set state_change_response is_active true 447 | ::wibble::return_200_json [::wibble::compile_json {dict} $state_change_response] 448 | } 449 | } 450 | 451 | 452 | } else { 453 | ::wibble::return_200_json {} 454 | } 455 | } 456 | } 457 | 458 | proc ::wibble::flushLog {state} { 459 | if { ![check_auth $state] } { 460 | return; 461 | } 462 | 463 | ::logging::flush_log 464 | 465 | ::wibble::return_200_json "" 466 | } 467 | 468 | 469 | # Define handlers 470 | 471 | ::wibble::handle /api/status state 472 | ::wibble::handle /api/flush flushLog 473 | ::wibble::handle /api/profile profile 474 | ::wibble::handle /api/shot history 475 | ::wibble::handle /api/help docs 476 | ::wibble::handle /api/v2/shot history_v2 477 | ::wibble::handle /api/v2/shots history_sdb 478 | ::wibble::handle / indexpage 479 | # Start a server and enter the event loop if not already there. 480 | 481 | catch { 482 | ::wibble::listen $::plugins::advanced_rest_api::settings(webserver_port) 483 | } 484 | 485 | } ;# main 486 | } 487 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # The idea 2 | Before I even received my Decent Espresso Machine I was searching for possibilities to integrate the machine with Home Automation systems. 3 | 4 | On the diaspora I stubled upon the Web API created by Johanna Schander (https://github.com/decentespresso/de1app/tree/main/de1plus/plugins/web_api) and also the more detailed API by Henrik Pejer (https://github.com/pejer/decentespresso_module_system/tree/modules/modules/pejer_web_api). The second one was integrated directly to the app. So it will break with every update. Since there is a good working plugin system I thought this should be used. 5 | 6 | I talked to both and with their "blessing" I combined both ideas and added mine to it. 7 | 8 | # What does it do? 9 | * get a simple or detailed state of the machine 10 | * change the state to standby or enable it 11 | * get, display or download shot files 12 | * get, download or upload profiles 13 | 14 | 15 | # How to use it? 16 | * Copy the files to the plugin folder into a folder called "advanced_rest_api". 17 | * Adjust the plugin settings in the settings file if needed. 18 | * Activate the plugin in the app settings. 19 | 20 | ## API 21 | There is a documentation about all available API endpoints in the attached json - which is also displayed in the Web UI. 22 | 23 | ## Web UI 24 | Navigate to http://DECENT-IP:8888 in your preferred browser. The port can be changed in the settings. 8080 should be prevented if it is used with the webcast feature. 25 | 26 | ## Home Assistant 27 | There is example configuration for Home Assistant (https://home-assistant.io) in the [homeassistant](./homeassistant) directory. 28 | 29 | ## Homebridge 30 | As requested I added a Homebridge accessory config. 31 | * install the Homebridge Http Switch Plugin (https://github.com/Supereg/homebridge-http-switch) 32 | * create a new device and add the example configuration 33 | 34 | Update: there is now a better way to integrate this plugin with Homebridge: https://github.com/muelmx/homebridge-decent-advanced-rest-api / https://www.npmjs.com/package/homebridge-decent-advanced-rest-api 35 | Many thanks to muelmx :-) 36 | 37 | 38 | ... please contact me for feature requests. 39 | 40 | # Disclaimer 41 | Of course the usage of this is at your own risk. It may break your machine. You are creating a web server with an unencypted connection in your network. Please be cautious. 42 | 43 | This is my first work in TCL - please be nice ;-) 44 | -------------------------------------------------------------------------------- /settings.tdb: -------------------------------------------------------------------------------- 1 | webserver_authentication_key myFancyAuthenticationKey 2 | webserver_authentication 0 3 | webserver_port 8888 4 | 5 | --------------------------------------------------------------------------------