1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
| --[[
=======================================================================
Dreame Vacuum <-> Home Assistant QuickApp for Fibaro HC3
=======================================================================
What this does:
- Polls a Home Assistant "vacuum" entity (your Dreame robot, exposed
via the HACS Dreame integration) over the HA REST API.
- Shows: current status, whether it's cleaning, whether it's
mopping, whether the current run was started by a schedule,
battery level, and (optionally) whether it needs maintenance
(low consumables: brushes / filter / mop pads).
- Buttons to Start / Pause / Return to dock.
- A Debug button that dumps the full raw entity JSON to the log,
so you can find the exact attribute names your integration uses.
SETUP:
1. Create a new QuickApp on your HC3 (Devices -> + -> Add device ->
Other devices -> Generic Device), then replace the default
template code with this whole file, and Save.
2. The header --%%u: lines at the top are NOT reliably auto-applied
by every HC3 firmware when pasting into a fresh Generic Device
template. If, after saving, the device has no label/buttons,
add them manually in the device's UI editor (not the code tab):
- six LABELS with ids: status, cleaning, mopping, scheduled,
battery, attention (each on its own line/row of the UI)
- a row of BUTTONS with ids: startCleaning, pauseCleaning,
returnToDock (each wired to call the QuickApp method of the
same name on click / onReleased)
- a row of MODE BUTTONS: setModeSweep, setModeMop, setModeBoth
(optionally also setModeMopAfterSweep), wired the same way
- a row of ROOM BUTTONS: one per room you want a quick-clean
button for, e.g. cleanOffice, cleanLivingRoom, cleanKitchen
(add more by copying the pattern in the code + a button)
- one BUTTON with id: debugDump (wired the same way)
3. Fill in the variables in the "USER CONFIG" section below.
4. Save & Start/Restart the QuickApp. Check the log for the first
poll result; the label should populate within a few seconds.
5. If "Mopping" / "Scheduled" don't reflect reality, press the
"Debug: dump entity" button, look at the logged JSON, and
tell me the attribute + value you see, so I can tune the
matching logic to your integration.
=======================================================================
]]
--%%name:Dreame Vacuum (HA)
--%%type:com.fibaro.genericDevice
-- NOTE: --%%u: UI-generating header lines were intentionally removed.
-- On this firmware they appear to get (re-)applied on code save and
-- can create duplicate UI elements that collide with ones configured
-- manually in the device's UI editor, causing button taps to silently
-- do nothing. All UI elements for this QuickApp must be added by hand
-- in the UI editor - see the SETUP notes above.
-------------------------------------------------------------------
-- USER CONFIG - fill these in
-------------------------------------------------------------------
-- HA_IP and HA_Bearer are NOT set here - they are read from HC3
-- Global Variables (Panels -> Variables) named "HA_IP" and
-- "HA_Bearer". Create those two global variables on your HC3 first
-- and fill them with your Home Assistant IP and Long-Lived Access
-- Token respectively.
local HA_PORT = "8123" -- Home Assistant port
-- The Dreame vacuum's object_id in Home Assistant - i.e. whatever
-- comes after "vacuum." in its entity id (vacuum.stoffie -> "stoffie").
-- All other entity ids below (select, sensors) are derived from this
-- automatically, since the Dreame HACS integration names every
-- related entity "<domain>.<VACUUM_NAME>_<thing>".
local VACUUM_NAME = "stoffie"
local VACUUM_ENTITY = "vacuum." .. VACUUM_NAME
local CLEANING_MODE_ENTITY = "select." .. VACUUM_NAME .. "_cleaning_mode"
local SUCTION_LEVEL_ENTITY = "select." .. VACUUM_NAME .. "_suction_level"
local CLEANING_ROUTE_ENTITY = "select." .. VACUUM_NAME .. "_cleaning_route"
local SELECTED_MAP_ENTITY = "select." .. VACUUM_NAME .. "_selected_map"
local CLEANING_PROGRESS_ENTITY = "sensor." .. VACUUM_NAME .. "_cleaning_progress"
local SELF_CLEAN_TIME_ENTITY = "number." .. VACUUM_NAME .. "_self_clean_time"
local HA_USE_HTTPS = true -- true = https://, false = http://
local HA_IGNORE_CERT_ERRORS = true -- set true if HA uses a self-signed certificate
local HA_SCHEME = HA_USE_HTTPS and "https" or "http"
-- Optional: consumable/maintenance sensors exposed by the integration.
-- Leave the table empty ( {} ) to skip maintenance checking entirely.
-- Derived from VACUUM_NAME the same way - uncomment the ones you want.
local CONSUMABLE_ENTITIES = {
["Main brush"] = "sensor." .. VACUUM_NAME .. "_main_brush_time_left",
["Side brush"] = "sensor." .. VACUUM_NAME .. "_side_brush_time_left",
["Filter"] = "sensor." .. VACUUM_NAME .. "_filter_time_left",
["Sensor"] = "sensor." .. VACUUM_NAME .. "_sensor_dirty_time_left",
["Wheel"] = "sensor." .. VACUUM_NAME .. "_wheel_dirty_time_left",
}
local MAINTENANCE_THRESHOLD = 10 -- % remaining life below which we flag "needs attention"
-- IDs of the 3 dropdown ("Select") UI elements you added, so we can
-- push the vacuum's *current* setting into them on every poll (and
-- right after you change one). These must exactly match the "ID"
-- field you set for each dropdown in the UI editor - e.g. the
-- default auto-generated ones look like "select_ID_10". Check yours
-- and update these three lines to match.
local CLEANING_MODE_UI_ID = "select_ID_10"
local SUCTION_LEVEL_UI_ID = "select_ID_11"
local CLEANING_ROUTE_UI_ID = "select_ID_12"
local SELECTED_MAP_UI_ID = "select_ID_13"
-- ID of an OPTIONAL slider UI element for mop washing time (10-50
-- minutes, per your device's min/max). Only needed if you add a
-- Slider element - the preset buttons below work without it.
local SELF_CLEAN_TIME_UI_ID = "selfCleanTimeSlider"
-- This firmware's Slider element is fixed at a 0-100 range with no
-- editable min/max, but the real device only accepts 10-50 minutes.
-- These two functions map between the slider's 0-100 position and
-- the actual minutes value, so slider-left = 10 min, slider-right =
-- 50 min, instead of most of the slider's travel being invalid.
local SELF_CLEAN_TIME_MIN = 10
local SELF_CLEAN_TIME_MAX = 50
local function sliderToMinutes(sliderValue)
sliderValue = math.max(0, math.min(100, tonumber(sliderValue) or 0))
local minutes = SELF_CLEAN_TIME_MIN + (sliderValue / 100) * (SELF_CLEAN_TIME_MAX - SELF_CLEAN_TIME_MIN)
return math.floor(minutes + 0.5)
end
local function minutesToSlider(minutes)
minutes = tonumber(minutes) or SELF_CLEAN_TIME_MIN
local sliderValue = (minutes - SELF_CLEAN_TIME_MIN) / (SELF_CLEAN_TIME_MAX - SELF_CLEAN_TIME_MIN) * 100
return math.floor(sliderValue + 0.5)
end
-- Room name -> segment id, GROUPED BY MAP/FLOOR, since the same
-- room name can have a different segment id per floor (e.g.
-- "Corridor" exists on all 3 floors with different ids). The outer
-- keys must exactly match the "Map/floor" dropdown's option values
-- (select.<name>_selected_map). Segment ids come straight from the
-- "rooms" attribute of the vacuum entity - re-check there if you
-- rename/add rooms on the robot later.
local ROOMS_BY_MAP = {
["Begane Grond"] = {
["Office"] = 1,
["Living Room"] = 2,
["Corridor"] = 3,
["Dining Hall"] = 4,
["Kitchen"] = 5,
},
["1ste verdieping"] = {
["Slaapkamer 1"] = 1,
["Slaapkamer 2"] = 3,
["Slaapkamer 3"] = 4,
["Corridor"] = 5,
["Bathroom"] = 6,
},
["2e verdieping"] = {
["Bathroom"] = 1,
["Corridor"] = 2,
["Closet"] = 3,
["Ouders Slaapkamer"] = 4,
},
}
local POLL_INTERVAL = 20 -- seconds between polls
-------------------------------------------------------------------
-- Like self:updateView, but silently does nothing if the target UI
-- element doesn't exist (e.g. you removed an optional label/dropdown
-- from the UI editor). Prevents log warnings from piling up.
function QuickApp:safeUpdateView(id, prop, value)
pcall(function() self:updateView(id, prop, value) end)
end
function QuickApp:onInit()
self:debug("Dreame HA QuickApp starting...")
self.http = net.HTTPClient()
-- Read connection details from HC3 Global Variables instead of
-- hardcoding them in the code.
self.ipaddr = hub.getGlobalVariable("HA_IP")
self.token = hub.getGlobalVariable("HA_Bearer")
if not self.ipaddr or self.ipaddr == "" then
self:error("Global Variable 'HA_IP' is missing or empty - please create/fill it")
end
if not self.token or self.token == "" then
self:error("Global Variable 'HA_Bearer' is missing or empty - please create/fill it")
end
self.info = {
status = "-", cleaning = "-", mopping = "-",
scheduled = "-", battery = "-", attention = "n/a", mode = "-",
selfCleanTime = "-", map = "-"
}
self:renderLabel()
self:updateData()
end
-- Pushes each field to its own label. Labels don't render HTML in
-- this widget, so we use separate label elements instead of <br>.
function QuickApp:renderLabel()
local i = self.info
self:safeUpdateView("status", "text", "Status: " .. tostring(i.status))
self:safeUpdateView("mode", "text", "Program: " .. tostring(i.mode))
self:safeUpdateView("cleaning", "text", "Cleaning: " .. tostring(i.cleaning))
self:safeUpdateView("mopping", "text", "Mopping: " .. tostring(i.mopping))
self:safeUpdateView("scheduled", "text", "Scheduled task: " .. tostring(i.scheduled))
self:safeUpdateView("battery", "text", "Battery: " .. tostring(i.battery) .. "%")
self:safeUpdateView("attention", "text", "Needs attention: " .. tostring(i.attention))
self:safeUpdateView("washTime", "text", "Mop wash time: " .. tostring(i.selfCleanTime) .. " min")
self:safeUpdateView("map", "text", "Map/floor: " .. tostring(i.map))
end
-------------------------------------------------------------------
-- Generic HA REST helpers
-------------------------------------------------------------------
function QuickApp:haGet(path, callback)
local url = HA_SCHEME .. "://" .. self.ipaddr .. ":" .. HA_PORT .. "/api/" .. path
self.http:request(url, {
options = {
method = "GET",
checkCertificate = not HA_IGNORE_CERT_ERRORS,
headers = {
["Authorization"] = "Bearer " .. self.token,
["Content-Type"] = "application/json"
}
},
success = function(response)
if response.status == 200 then
local ok, data = pcall(json.decode, response.data)
if ok then
callback(data)
else
self:error("JSON decode error on " .. path .. ": " .. tostring(data))
callback(nil)
end
else
self:error("HA API GET " .. path .. " returned status " .. tostring(response.status))
callback(nil)
end
end,
error = function(err)
self:error("HA API GET " .. path .. " failed: " .. tostring(err))
callback(nil)
end
})
end
function QuickApp:haCallService(domain, service, entityId, extra)
local url = HA_SCHEME .. "://" .. self.ipaddr .. ":" .. HA_PORT .. "/api/services/" .. domain .. "/" .. service
local body = { entity_id = entityId }
if extra then
for k, v in pairs(extra) do body[k] = v end
end
local payload = json.encode(body)
self.http:request(url, {
options = {
method = "POST",
checkCertificate = not HA_IGNORE_CERT_ERRORS,
headers = {
["Authorization"] = "Bearer " .. self.token,
["Content-Type"] = "application/json"
},
data = payload
},
success = function(response)
self:debug("Called service " .. domain .. "." .. service .. " -> status " .. tostring(response.status))
-- refresh state shortly after issuing a command
fibaro.setTimeout(2500, function() self:updateData() end)
end,
error = function(err)
self:error("Service call " .. domain .. "." .. service .. " failed: " .. tostring(err))
end
})
end |