Read device settings and active profile via BLE - #234
Open
Koky05 wants to merge 13 commits into
Open
Conversation
added 7 commits
April 17, 2026 10:58
…atus text
- Use model name from MachinesModels.json instead of hardcoded "Prima Donna"
- Load profile names from machine via 0xA4 command, default to profile 1
- Build dynamic beverage list from machine model recipes (17+ beverages)
instead of hardcoded 9-item enum, with generic start/stop command builder
- Fix parse_switches byte offset: use data[5]|data[6]<<8 to match
parse_monitor_data (was incorrectly using data[7] for high byte)
- Make DEVICE_STATUS values human-readable ("Descaling needed" not
"descale_alarm")
- Default initial status to "Ready" instead of "coffee_beans_empty"
- Fix ImageEntity by setting image_last_updated timestamp
- Track active profile via 0xA9 response handler
- Update service schema to accept dynamic beverage names
- Add status icon that changes based on machine state
- Remove dead legacy fallback code in beverage_start/beverage_cancel where string-vs-enum comparison would never match - Add warning log for unknown beverage names - Validate beverage service schema against device's available_beverages list instead of accepting any string
Replace magic string 'none' with BEVERAGE_NONE constant from const.py across device.py and select.py to avoid typos and keep components in sync.
- Break long lines to comply with 79-char limit (E501) - Fix blank line counts (E302, E303, E305) - Add two spaces before inline comments (E261) - Add whitespace around arithmetic operators (E226) - Remove unused datetime import from image.py (F401) - Remove duplicate async_added_to_hass definition (F811) - Clean up verbose comments
Read device settings from the machine via BLE protocol discovery: - Query active profile via 0xA5+0xF0 command - Read per-profile settings via 0x90+0xF0: switches (eco, sounds, cup light), auto power off, water temperature, water hardness - Parse 0x90 responses to update device state in real-time - Switch and select entities now reflect actual machine settings - Settings are read once after first successful statistics update Fix coffee with milk total: - Combine stat IDs 3001+3003 (matching machine display value) Improve statistics reliability: - Wrap individual stats requests in try/except so a single BLE timeout doesn't prevent subsequent requests or settings read
Contributor
Reviewer's GuideImplements BLE settings readback and active profile detection, wires parsed values into switches/selects, improves statistics aggregation (including coffee-with-milk total), makes beverage handling recipe-aware, and hardens BLE statistics/settings polling against timeouts. Sequence diagram for BLE statistics and settings pollingsequenceDiagram
participant HA as HomeAssistant
participant Device as DelongiPrimadonna
participant BLE as BLEStack
participant Machine as CoffeeMachine
HA->>Device: update_statistics()
alt stats lock held
Device-->>HA: return
else
Device->>Device: check throttle window
alt too soon
Device-->>HA: return
else
loop stats_ranges
Device->>BLE: get_statistics(start,count)
BLE->>Machine: A1 stats request
alt timeout or error
Machine--x BLE: no response
BLE--x Device: exception
Device->>Device: catch Exception
else success
Machine-->>BLE: A2 stats response
BLE-->>Device: notification(value)
Device->>Device: _handle_data(answer_id=0xA2)
Device->>Device: _parse_statistics(data)
end
Device->>Device: asyncio.sleep(0.3)
end
opt settings not yet loaded
Device->>Device: _read_settings()
Device->>BLE: send_command([0x0D,0x05,0xA5,0xF0,...])
BLE->>Machine: active profile request (A5)
Machine-->>BLE: A5 response (profile_id)
BLE-->>Device: notification(value)
Device->>Device: _handle_data(answer_id=0xA5)
Device->>Device: active_profile_id = value[4]
loop settings_groups (3F,3E,3D,32)
Device->>BLE: send_command([0x0D,0x0B,0x90,0xF0,pid,group,...])
BLE->>Machine: 90 settings request
Machine-->>BLE: 90 settings response
BLE-->>Device: notification(value)
Device->>Device: _handle_data(answer_id=0x90)
Device->>Device: _parse_settings_response(data)
Device->>Device: asyncio.sleep(0.3)
end
Device->>Device: _settings_loaded = True
end
end
end
Sequence diagram for recipe-aware beverage start and cancelsequenceDiagram
actor User
participant HA as HomeAssistant
participant Device as DelongiPrimadonna
participant Machine as CoffeeMachine
User->>HA: call make_beverage(beverage_name)
HA->>Device: beverage_start(beverage_name)
alt beverage_name == BEVERAGE_NONE
Device-->>HA: return
else
Device->>Device: recipe = _recipe_map.get(name)
alt recipe found
Device->>Device: rid = recipe.id
Device->>Device: legacy = RECIPE_ID_TO_BEVERAGE.get(rid)
alt legacy exists and command available
Device->>Machine: send_command(BEVERAGE_COMMANDS[legacy].on)
else dynamic
Device->>Device: cmd = _build_start_command(rid,coffee_qty,milk_qty)
Device->>Machine: send_command(cmd)
end
Device->>Device: cooking = beverage_name
else recipe missing
Device-->>HA: log warning Unknown beverage
end
end
User->>HA: cancel beverage
HA->>Device: beverage_cancel()
alt cooking == BEVERAGE_NONE
Device-->>HA: return
else
Device->>Device: recipe = _recipe_map.get(cooking)
alt recipe found
Device->>Device: cmd = _build_stop_command(recipe.id)
Device->>Machine: send_command(cmd)
else
Device-->>HA: log warning Cannot cancel unknown beverage
end
Device->>Device: cooking = BEVERAGE_NONE
end
Class diagram for updated Delonghi device, sensors, selects, and switchesclassDiagram
class DelongiPrimadonna {
+str mac
+str name
+str product_code
+str model
+str image_url
+str friendly_name
+str status
+str cooking
+bool connected
+bool notify
+str steam_nozzle
+int service
+DeviceSwitches switches
+list~MachineSwitch~ active_switches
+bool sync_time
+int _n_profiles
+list~str~ profiles
+bool _profiles_loaded
+int active_profile_id
+bool _settings_loaded
+int auto_off_index
+int water_hardness_index
+int water_temperature_index
+dict~str, dict~ _recipe_map
+list~str~ available_beverages
+dict~int, int~ statistics
+float _last_stats_request
+Lock _stats_lock
+__init__(config, hass)
+disconnect()
+_handle_data(sender, value)
+_parse_settings_response(data)
+_read_settings()
+_parse_statistics(data)
+update_statistics()
+get_statistics(start_index, count)
+beverage_start(beverage)
+beverage_cancel()
}
class DelongiPrimadonnaStatisticsSensor {
-int _param_id
-str _attr_name
-str _attr_unique_id
-str _attr_translation_key
-str _attr_native_unit_of_measurement
-str _attr_icon
-float _attr_native_value
+__init__(device, hass, sensor_type, param_id, name, native_unit_of_measurement, icon)
+async_added_to_hass()
+native_value
+icon
+async_update()
}
class ProfileSelect {
-str _attr_current_option
+options() list~str~
+current_option() str
+async_select_option(option)
+entity_category
}
class BeverageSelect {
-str _attr_current_option
+options() list~str~
+async_added_to_hass()
+async_select_option(option)
}
class EnergySaveModeSelect {
-str _attr_current_option
+current_option() str
+async_added_to_hass()
+async_select_option(option)
}
class WaterHardnessSelect {
-str _attr_current_option
+current_option() str
+async_added_to_hass()
+async_select_option(option)
}
class WaterTemperatureSelect {
-str _attr_current_option
+current_option() str
+async_added_to_hass()
+async_select_option(option)
}
class EnergySaveModeSwitch {
-bool _attr_is_on
+async_added_to_hass()
+is_on bool
}
class SoundsSwitch {
-bool _attr_is_on
+async_added_to_hass()
+is_on bool
}
class CommandsBuilder {
+_build_start_command(recipe_id, coffee_qty, milk_qty) list~int~
+_build_stop_command(recipe_id) list~int~
}
DelongiPrimadonna "1" o-- "many" DelongiPrimadonnaStatisticsSensor : provides_statistics_for
DelongiPrimadonna "1" o-- "1" ProfileSelect : exposes_profiles
DelongiPrimadonna "1" o-- "1" BeverageSelect : exposes_beverages
DelongiPrimadonna "1" o-- "1" EnergySaveModeSelect : exposes_auto_off
DelongiPrimadonna "1" o-- "1" WaterHardnessSelect : exposes_hardness
DelongiPrimadonna "1" o-- "1" WaterTemperatureSelect : exposes_temperature
DelongiPrimadonna "1" o-- "1" EnergySaveModeSwitch : exposes_energy_save
DelongiPrimadonna "1" o-- "1" SoundsSwitch : exposes_sounds
DelongiPrimadonna ..> CommandsBuilder : uses_for_dynamic_commands
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
update_statistics, the broadexcept Exception: passaroundget_statisticscalls will silently hide protocol or parsing issues; consider at least logging the exception (with the start/count range) at debug level so intermittent failures can be diagnosed. - The fixed
asyncio.sleep(0.3)delays in_read_settingsand before reading the active profile are effectively magic timing constants; it would be helpful to centralize or document why 300 ms is required here and whether it should be adjustable for different devices/connection conditions.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `update_statistics`, the broad `except Exception: pass` around `get_statistics` calls will silently hide protocol or parsing issues; consider at least logging the exception (with the start/count range) at debug level so intermittent failures can be diagnosed.
- The fixed `asyncio.sleep(0.3)` delays in `_read_settings` and before reading the active profile are effectively magic timing constants; it would be helpful to centralize or document why 300 ms is required here and whether it should be adjustable for different devices/connection conditions.
## Individual Comments
### Comment 1
<location path="custom_components/delonghi_primadonna/device.py" line_range="954-960" />
<code_context>
-
- # Optional: Request tea/other beverages if needed
- # await self.get_statistics(3025, 1) # Tea counter
+ for start, count in [
+ (100, 10), (110, 10), (3000, 10), (3077, 4),
+ ]:
+ try:
+ await self.get_statistics(start, count)
+ await asyncio.sleep(0.3)
+ except Exception: # noqa: BLE001
+ pass
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Swallowing all exceptions when fetching statistics can hide real issues and make debugging harder.
The `update_statistics` loop catches `Exception` and then `pass`es, so repeated failures (e.g. protocol or transport issues) will never be visible and statistics may remain stale. Please either log the exception (including the `start`/`count` range) or restrict the `except` to the specific, expected error types.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
added 6 commits
April 17, 2026 11:12
Release the BLE connection after completing each operation batch (statistics update, settings read, beverage commands, etc.) instead of maintaining a persistent connection. This prevents the DeLonghi integration from blocking other BLE integrations (Sonicare, Tuya, Shelly, etc.) from connecting. - Add disconnect() call after update_statistics, get_device_name, and all individual command methods - Change device_tracker to use BLE advertisement detection instead of persistent connection polling
- Log failed statistics requests at debug level with start/count range instead of silently swallowing exceptions - Extract BLE_CMD_DELAY constant (0.3s) to document and centralize the inter-command delay required by the machine
Return restored state as fallback when the device statistics dict has not yet been populated (machine off or BLE unreachable). Previously native_value always returned None when stats were empty, causing sensors to show "unknown" despite having a valid restored state.
The connected flag is always False now since we disconnect after each operation. Remove the guard so update_statistics is triggered on every sensor poll cycle (throttled internally to 60 seconds).
- Replace raw BleakClient with establish_connection() from bleak_retry_connector for proper BLE slot management - Add SCAN_INTERVAL = 120s to sensor.py to reduce adapter contention with other BLE integrations (was default 30s) These changes fix BLE adapter conflicts when running alongside other BLE integrations (e.g. Philips Sonicare, Tuya).
Owner
|
Ready to merge after the conflict resolution |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
0xA5+0xF0command0x90+0xF0with profile index: switches (eco mode, sounds, cup light), auto power off, water temperature, water hardness0x90and0xA5responses to update device stateProtocol Discovery
Settings are stored per-profile on the machine. The read command format:
Groups:
0x3F=switches,0x3E=auto-off,0x3D=temperature,0x32=hardnessActive profile query:
[0x0D, 0x05, 0xA5, 0xF0, CRC, CRC]→ returns profile ID at byte[4]Depends on
Test plan
Tested on DeLonghi Dinamica PLUS ECAM 370.95.S
Summary by Sourcery
Add dynamic, model-based beverage handling and BLE-backed settings/profile readout to better reflect the machine’s actual state and capabilities.
New Features:
Bug Fixes:
Enhancements: