-
-
Notifications
You must be signed in to change notification settings - Fork 1
Integration rewrite: New sensors & attributes and code improvements #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughPer-entry, per-sensor concurrent recipe resolution and attribute extraction implemented; sensors now come from entry.options and carry per-config IDs/names; a multi-step Options Flow to add/edit/remove custom sensors added; manifest bumped to 2.0.0 and strings/translations expanded for the options UI. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant HA as Home Assistant
participant Flow as Config/Options Flow
participant Entry as ConfigEntry
participant CC as Chefkoch Integration
participant Coord as DataUpdateCoordinator
participant Sensor as ChefkochSensor
User->>HA: Install / Open integration
HA->>Flow: Start Config / Options Flow
Flow-->>HA: Create/Update Entry with options.sensors
HA->>CC: async_setup_entry(entry)
CC->>Coord: init(update_method=async_update_data(hass, entry))
Coord->>CC: Call update_method (initial / scheduled)
CC->>CC: For each sensor in entry.options.sensors -> start fetch task
CC->>CC: _fetch_recipe_url(sensor_config) / extract_recipe_attributes(url)
CC-->>Coord: Return aggregated map {sensor_id: data_or_error}
Coord-->>HA: Coordinator ready
HA->>Sensor: Create entity per sensor_config
Sensor-->>User: Entities expose native_value and attributes
sequenceDiagram
autonumber
actor User
participant HA as Home Assistant
participant Flow as Options Flow
participant Entry as ConfigEntry
participant Coord as DataUpdateCoordinator
User->>HA: Open Integration Options
HA->>Flow: async_step_init
alt Add Defaults
User->>Flow: Choose add_defaults
Flow->>Entry: Append missing DEFAULT_SENSORS
else Add Sensor
User->>Flow: Choose add_sensor
Flow->>Flow: Show form (get_search_schema)
Flow->>Entry: Append new sensor with generated UUID
else Edit Sensor
User->>Flow: Choose edit_sensor
Flow->>Flow: Select sensor -> edit_sensor_form
Flow->>Entry: Update selected sensor data
else Remove Sensor
User->>Flow: Choose remove_sensor
Flow->>Entry: Remove selected sensors
end
Entry-->>Coord: Options updated
Coord->>Coord: Next refresh uses updated sensors list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (9)
custom_components/chefkoch_ha/config_flow.py (3)
53-67
: Default sensors UX: consider auto-seeding on first run.To reduce friction, pre-populate defaults on initial create (or via a migration) instead of requiring an options step.
If desired, I can draft an
async_migrate_entry
that seeds defaults when upgrading existing entries.
68-92
: Localize difficulty values or store canonical values.
vol.In(['', 'Einfach', 'Normal', 'Schwer'])
hardcodes German terms for all locales. Prefer canonical values (e.g., '', 'easy', 'normal', 'hard') and translate labels via strings.json, or make the enum locale-agnostic with selectors.
105-112
: Abort reason may mislead on missing ID.If a selected sensor ID isn’t found, a more specific abort like
sensor_not_found
would be clearer thanno_sensors
.I can add the key to translations and wire it up if you want.
custom_components/chefkoch_ha/translations/de.json (1)
25-31
: Difficulty values are German while EN UI exists.If you adopt canonical values in code, translate the visible labels here to keep both locales consistent.
custom_components/chefkoch_ha/sensor.py (3)
25-39
: Entity naming/ID handling looks good; consider future-proof unique_id.Prefixing non-Chefkoch names is nice. For unique_id, consider including the ConfigEntry id to avoid collisions if multi-instance ever becomes allowed.
Example:
- self._attr_unique_id = f"chefkoch_{sensor_config['id']}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_chefkoch_{sensor_config['id']}"
45-50
: Return None for unknown to mark state as unavailable.Returning "unknown" as a string sets a real state. Prefer None to indicate unknown/unavailable to HA.
- data = self.coordinator.data.get(self.sensor_id, {}) - return data.get("title", "unknown") + data = self.coordinator.data.get(self.sensor_id, {}) + return data.get("title")
51-64
: Attributes filtering is good; add device_info for better UX.Nice filtering. Adding device_info groups entities under a device in the UI.
class ChefkochSensor(CoordinatorEntity, SensorEntity): @@ self._attr_unique_id = f"chefkoch_{sensor_config['id']}" + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, "chefkoch")}, + "name": "Chefkoch", + "manufacturer": "Chefkoch", + }custom_components/chefkoch_ha/translations/en.json (1)
25-31
: Difficulty values should match code representation.If you switch to canonical difficulty values, translate the labels here accordingly.
custom_components/chefkoch_ha/__init__.py (1)
79-105
: Prefer logging.exception and avoid blind except.Use
_LOGGER.exception
to capture tracebacks, and scope exceptions more narrowly if possible.- try: + try: recipe = Recipe(recipe_url) @@ - except Exception as e: - _LOGGER.error(f"Failed to extract attributes for URL {recipe_url}: {e}") - return {"status": "error", "error_message": str(e)} + except Exception: + _LOGGER.exception("Failed to extract attributes for URL %s", recipe_url) + return {"status": "error", "error_message": "exception"}Also apply similar change above when fetching per sensor:
- except Exception as e: - _LOGGER.error("Error fetching data for sensor %s: %s", sensor_config['name'], e, exc_info=True) - data[sensor_id] = {"title": "Error", "status": "error", "error_message": str(e)} + except Exception: + _LOGGER.exception("Error fetching data for sensor %s", sensor_config['name']) + data[sensor_id] = {"title": "Error", "status": "error", "error_message": "exception"}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
custom_components/chefkoch_ha/__init__.py
(1 hunks)custom_components/chefkoch_ha/config_flow.py
(1 hunks)custom_components/chefkoch_ha/manifest.json
(1 hunks)custom_components/chefkoch_ha/sensor.py
(1 hunks)custom_components/chefkoch_ha/strings.json
(1 hunks)custom_components/chefkoch_ha/translations/de.json
(1 hunks)custom_components/chefkoch_ha/translations/en.json
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
custom_components/chefkoch_ha/__init__.py (1)
custom_components/chefkoch_ha/sensor.py (2)
sensor_id
(41-43)async_setup_entry
(9-19)
custom_components/chefkoch_ha/sensor.py (1)
custom_components/chefkoch_ha/__init__.py (1)
async_setup_entry
(106-125)
🪛 Ruff (0.13.1)
custom_components/chefkoch_ha/__init__.py
102-102: Do not catch blind exception: Exception
(BLE001)
103-103: Use logging.exception
instead of logging.error
Replace with exception
(TRY400)
custom_components/chefkoch_ha/config_flow.py
14-14: Unused method argument: user_input
(ARG002)
37-37: Unused method argument: user_input
(ARG002)
53-53: Unused method argument: user_input
(ARG002)
79-79: Consider [*self.current_sensors, new_sensor]
instead of concatenation
Replace with [*self.current_sensors, new_sensor]
(RUF005)
🪛 GitHub Actions: Validate with hassfest
custom_components/chefkoch_ha/strings.json
[error] 1-1: Invalid strings.json: extra keys not allowed @ data['options']['step']['init']['menu']. Got {'add_defaults': 'Add Default Sensors (Random, Daily, Vegan)', 'add_sensor': 'Add a new Recipe Sensor', 'edit_sensor': 'Edit an existing Sensor', 'remove_sensor': 'Remove a Sensor'}
🔇 Additional comments (9)
custom_components/chefkoch_ha/config_flow.py (3)
12-13
: Config flow version noted.VERSION=1 is fine; no issues.
16-20
: Single-instance guard is correct.Aborting on subsequent setups prevents duplicate entries. Good.
21-26
: Options flow hook is correct.Static
async_get_options_flow
returning the handler is the right pattern.custom_components/chefkoch_ha/sensor.py (1)
13-19
: Graceful handling when no sensors configured.Warning and early return are fine.
custom_components/chefkoch_ha/__init__.py (4)
21-44
: Concurrent per-sensor processing looks correct.Using gather with per-sensor try/except and thread offloading for blocking calls is appropriate.
45-77
: Retriever selection is fine; minor nit on falsey filtering.Filtering out empty values for SearchRetriever is good. No functional issues spotted.
110-116
: Coordinator wiring is good.Passing the entry to the update method via lambda and daily scan interval is fine.
127-136
: Options reload listener is correct; unload cleans up.Listener + platform reload and data cleanup look good.
custom_components/chefkoch_ha/manifest.json (1)
17-17
: Approve — version bump and dependency compatibility confirmed.
python-chefkoch==2.1.0 is published on PyPI and declares requires_python "<4.0,>=3.8" (compatible with Home Assistant's supported Python); manifest "version": "2.0.0" is appropriate.
async def async_step_init(self, user_input=None): | ||
"""Manage the options menu.""" | ||
menu_options = ["add_sensor"] | ||
if self.current_sensors: | ||
menu_options.extend(["edit_sensor", "remove_sensor"]) | ||
|
||
default_ids = {"random", "daily", "vegan"} | ||
current_ids = {s.get("id") for s in self.current_sensors} | ||
if not default_ids.issubset(current_ids): | ||
menu_options.insert(0, "add_defaults") | ||
|
||
return self.async_show_menu( | ||
step_id="init", | ||
menu_options=menu_options, | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix strings schema: use 'menu_options' in strings.json/translations.
async_show_menu
expects translations under <domain>.options.step.<step_id>.menu_options.<item>
. Hassfest is failing on strings.json because it uses menu
instead of menu_options
.
Apply this diff (strings.json; similar change needed in en.json and de.json):
- "init": {
- "title": "Manage Chefkoch Sensors",
- "menu": {
+ "init": {
+ "title": "Manage Chefkoch Sensors",
+ "menu_options": {
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)",
"add_sensor": "Add a new Recipe Sensor",
"edit_sensor": "Edit an existing Sensor",
"remove_sensor": "Remove a Sensor"
}
},
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async def async_step_init(self, user_input=None): | |
"""Manage the options menu.""" | |
menu_options = ["add_sensor"] | |
if self.current_sensors: | |
menu_options.extend(["edit_sensor", "remove_sensor"]) | |
default_ids = {"random", "daily", "vegan"} | |
current_ids = {s.get("id") for s in self.current_sensors} | |
if not default_ids.issubset(current_ids): | |
menu_options.insert(0, "add_defaults") | |
return self.async_show_menu( | |
step_id="init", | |
menu_options=menu_options, | |
) | |
"init": { | |
"title": "Manage Chefkoch Sensors", | |
"menu_options": { | |
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | |
"add_sensor": "Add a new Recipe Sensor", | |
"edit_sensor": "Edit an existing Sensor", | |
"remove_sensor": "Remove a Sensor" | |
} | |
}, |
🧰 Tools
🪛 Ruff (0.13.1)
37-37: Unused method argument: user_input
(ARG002)
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/config_flow.py around lines 37 to 52, the
translations keys used for async_show_menu are incorrect (they use "menu"
instead of the required "menu_options"); update the integration's strings.json
(and corresponding en.json and de.json) to move/rename the keys under
options.step.init.menu_options.<item> (e.g. change "options": { "step": {
"init": { "menu": { ... }}}} to "options": { "step": { "init": { "menu_options":
{ ... }}}}), ensuring each menu entry label key matches the menu_options entries
used by async_show_menu so Hassfest validates.
"title": "Manage Chefkoch Sensors", | ||
"menu": { | ||
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | ||
"add_sensor": "Add a new Recipe Sensor", | ||
"edit_sensor": "Edit an existing Sensor", | ||
"remove_sensor": "Remove a Sensor" | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix hassfest error: replace 'menu' with 'menu_options'.
Pipeline error indicates invalid key menu
. Use menu_options
for menu steps.
Apply this diff:
- "init": {
- "title": "Manage Chefkoch Sensors",
- "menu": {
+ "init": {
+ "title": "Manage Chefkoch Sensors",
+ "menu_options": {
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)",
"add_sensor": "Add a new Recipe Sensor",
"edit_sensor": "Edit an existing Sensor",
"remove_sensor": "Remove a Sensor"
}
},
Please re-run hassfest after the change to confirm the pipeline passes.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="custom_components/chefkoch_ha/strings.json"
echo "Checking file: $FILE"
if [ -f "$FILE" ]; then
echo "---- file content (first 200 lines) ----"
nl -ba "$FILE" | sed -n '1,200p'
echo "---- end file content ----"
else
echo "File not found: $FILE"
fi
echo
echo "---- Searching repo for '\"menu\" :' occurrences ----"
rg -n --hidden -S '"menu"\s*:' || echo "no matches for \"menu\""
echo
echo "---- Searching repo for '\"menu_options\" :' occurrences ----"
rg -n --hidden -S '"menu_options"\s*:' || echo "no matches for \"menu_options\""
Length of output: 139
🏁 Script executed:
#!/bin/bash
FILE="custom_components/chefkoch_ha/strings.json"
echo "Checking file: $FILE"
if [ -f "$FILE" ]; then
echo "---- file content (first 500 lines) ----"
if command -v sed >/dev/null 2>&1; then
sed -n '1,500p' "$FILE"
elif command -v python3 >/dev/null 2>&1; then
python3 - <<PY
with open("$FILE", "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
if i > 500:
break
print(f"{i}\t{line.rstrip()}")
PY
else
head -n 500 "$FILE" || cat "$FILE"
fi
else
echo "File not found: $FILE"
exit 0
fi
echo
echo "---- Searching for '\"menu\" :' and '\"menu_options\" :' in the file ----"
if command -v rg >/dev/null 2>&1; then
rg -n --hidden -S '"menu"\s*:' "$FILE" || echo "no matches for \"menu\" in $FILE"
rg -n --hidden -S '"menu_options"\s*:' "$FILE" || echo "no matches for \"menu_options\" in $FILE"
else
python3 - <<PY
import re, sys
pat1=re.compile(r'"menu"\s*:')
pat2=re.compile(r'"menu_options"\s*:')
f="$FILE"
try:
with open(f, 'r', encoding='utf-8') as fh:
for i, l in enumerate(fh, 1):
if pat1.search(l):
print(f"{f}:{i}:{l.rstrip()}")
with open(f, 'r', encoding='utf-8') as fh:
for i, l in enumerate(fh, 1):
if pat2.search(l):
print(f"{f}:{i}:{l.rstrip()}")
except Exception as e:
print("error reading file:", e)
PY
fi
Length of output: 1860
Fix hassfest: replace "menu" with "menu_options" in custom_components/chefkoch_ha/strings.json
Hassfest flags an invalid key "menu" under options.step.init — change it to "menu_options" and re-run hassfest.
- "init": {
- "title": "Manage Chefkoch Sensors",
- "menu": {
+ "init": {
+ "title": "Manage Chefkoch Sensors",
+ "menu_options": {
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)",
"add_sensor": "Add a new Recipe Sensor",
"edit_sensor": "Edit an existing Sensor",
"remove_sensor": "Remove a Sensor"
}
},
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"title": "Manage Chefkoch Sensors", | |
"menu": { | |
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | |
"add_sensor": "Add a new Recipe Sensor", | |
"edit_sensor": "Edit an existing Sensor", | |
"remove_sensor": "Remove a Sensor" | |
} | |
}, | |
"init": { | |
"title": "Manage Chefkoch Sensors", | |
"menu_options": { | |
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | |
"add_sensor": "Add a new Recipe Sensor", | |
"edit_sensor": "Edit an existing Sensor", | |
"remove_sensor": "Remove a Sensor" | |
} | |
}, |
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/strings.json around lines 16 to 23, the
options.step.init object uses the invalid key "menu" which hassfest rejects;
rename the "menu" key to "menu_options" preserving its child entries
("add_defaults", "add_sensor", "edit_sensor", "remove_sensor") so the structure
matches Home Assistant's expected schema, then re-run hassfest to confirm the
warning is resolved.
"title": "Chefkoch-Sensoren verwalten", | ||
"menu": { | ||
"add_defaults": "Standard-Sensoren hinzufügen (Zufall, Täglich, Vegan)", | ||
"add_sensor": "Neuen Rezept-Sensor hinzufügen", | ||
"edit_sensor": "Bestehenden Sensor bearbeiten", | ||
"remove_sensor": "Sensor entfernen" | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align with strings schema: use 'menu_options'.
Mirror the strings.json change: rename menu
to menu_options
under options.step.init
.
Apply this diff:
- "menu": {
+ "menu_options": {
"add_defaults": "Standard-Sensoren hinzufügen (Zufall, Täglich, Vegan)",
"add_sensor": "Neuen Rezept-Sensor hinzufügen",
"edit_sensor": "Bestehenden Sensor bearbeiten",
"remove_sensor": "Sensor entfernen"
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"title": "Chefkoch-Sensoren verwalten", | |
"menu": { | |
"add_defaults": "Standard-Sensoren hinzufügen (Zufall, Täglich, Vegan)", | |
"add_sensor": "Neuen Rezept-Sensor hinzufügen", | |
"edit_sensor": "Bestehenden Sensor bearbeiten", | |
"remove_sensor": "Sensor entfernen" | |
} | |
}, | |
"title": "Chefkoch-Sensoren verwalten", | |
"menu_options": { | |
"add_defaults": "Standard-Sensoren hinzufügen (Zufall, Täglich, Vegan)", | |
"add_sensor": "Neuen Rezept-Sensor hinzufügen", | |
"edit_sensor": "Bestehenden Sensor bearbeiten", | |
"remove_sensor": "Sensor entfernen" | |
} | |
}, |
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/translations/de.json around lines 16 to 23, the
JSON key "menu" under options.step.init must be renamed to "menu_options" to
match the strings schema; update the key name only (keep the nested labels and
values unchanged) so the structure matches the updated strings.json.
"title": "Manage Chefkoch Sensors", | ||
"menu": { | ||
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | ||
"add_sensor": "Add a new Recipe Sensor", | ||
"edit_sensor": "Edit an existing Sensor", | ||
"remove_sensor": "Remove a Sensor" | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align with strings schema: use 'menu_options'.
Rename menu
to menu_options
under options.step.init
to match hassfest schema and strings.json.
- "menu": {
+ "menu_options": {
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)",
"add_sensor": "Add a new Recipe Sensor",
"edit_sensor": "Edit an existing Sensor",
"remove_sensor": "Remove a Sensor"
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"title": "Manage Chefkoch Sensors", | |
"menu": { | |
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | |
"add_sensor": "Add a new Recipe Sensor", | |
"edit_sensor": "Edit an existing Sensor", | |
"remove_sensor": "Remove a Sensor" | |
} | |
}, | |
"title": "Manage Chefkoch Sensors", | |
"menu_options": { | |
"add_defaults": "Add Default Sensors (Random, Daily, Vegan)", | |
"add_sensor": "Add a new Recipe Sensor", | |
"edit_sensor": "Edit an existing Sensor", | |
"remove_sensor": "Remove a Sensor" | |
} | |
}, |
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/translations/en.json around lines 16 to 23, the
translations object uses the key "menu" under options.step.init but
hassfest/strings.json expects "menu_options"; rename the "menu" property to
"menu_options" and preserve all child keys and values exactly (add_defaults,
add_sensor, edit_sensor, remove_sensor) so the strings schema matches and
localization continues to work.
Docstrings generation was requested by @FaserF. * #11 (comment) The following files were modified: * `custom_components/chefkoch_ha/__init__.py` * `custom_components/chefkoch_ha/config_flow.py` * `custom_components/chefkoch_ha/sensor.py`
Note Generated docstrings for this pull request at #12 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (7)
custom_components/chefkoch_ha/__init__.py (2)
33-35
: Use logging.exception for tracebacks; avoid blind Exception if possibleReplace error logging with logging.exception to capture stack traces. If you need to keep the broad except, annotate BLE001 or narrow exceptions.
Apply this diff:
- except Exception as e: - _LOGGER.error("Error during data fetching for sensor %s: %s", sensor_config['name'], e, exc_info=True) - data[sensor_id] = {"title": "Error fetching URL", "status": "error", "error_message": str(e)} + except Exception as e: # noqa: BLE001 + _LOGGER.exception("Error during data fetching for sensor %s: %s", sensor_config["name"], e) + data[sensor_id] = {"title": "Error fetching URL", "status": "error", "error_message": str(e)}
87-119
: Prefer logging.exception; narrow catches or annotate
- Switch to logging.exception for initialization failures.
- If broad except is necessary, annotate BLE001 or narrow to expected exceptions from chefkoch’s API.
Apply this diff:
def extract_recipe_attributes(recipe_url): @@ - try: - recipe = Recipe(recipe_url) - except Exception as e: - _LOGGER.error("Failed to initialize Recipe object for URL %s: %s", recipe_url, e) + try: + recipe = Recipe(recipe_url) + except Exception as e: # noqa: BLE001 + _LOGGER.exception("Failed to initialize Recipe object for URL %s: %s", recipe_url, e) return {"title": "Error loading recipe details", "url": recipe_url, "status": "error", "error_message": f"Could not parse recipe page: {e}"} @@ - def safe_get_attr(recipe_obj, attr_name, default=None): - try: - return getattr(recipe_obj, attr_name) - except Exception: + def safe_get_attr(recipe_obj, attr_name, default=None): + try: + return getattr(recipe_obj, attr_name) + except Exception: # noqa: BLE001 _LOGGER.debug("Could not get attribute '%s' for recipe %s", attr_name, recipe_obj.url) return defaultcustom_components/chefkoch_ha/strings.json (1)
23-37
: Optional: add titles for edit/remove steps for a cleaner UIConsider adding options.step.edit_sensor.title and options.step.remove_sensor.title to improve forms.
If you want this, I can draft the additions for all locales.
custom_components/chefkoch_ha/config_flow.py (4)
1-7
: Import copy for deep-copying defaultsNeeded for safe seeding of DEFAULT_SENSORS (see below).
Apply this diff:
import voluptuous as vol import uuid +import copy
8-21
: Schema: set a non-None default for ‘name’Avoid showing “None” as default in the form.
Apply this diff:
- vol.Required("name", default=sensor_data.get("name")): str, + vol.Required("name", default=sensor_data.get("name", "")): str,
43-49
: Minor: unused arg; make explicitRename unused parameter to underscore for clarity.
Apply this diff:
- async def async_step_init(self, user_input=None): + async def async_step_init(self, _user_input=None):
52-55
: Nit: avoid list concatenation in favor of unpack for readabilityPurely stylistic; optional.
Apply this diff:
- updated_sensors = self.current_sensors + [new_sensor] + updated_sensors = [*self.current_sensors, new_sensor]
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
custom_components/chefkoch_ha/__init__.py
(2 hunks)custom_components/chefkoch_ha/config_flow.py
(1 hunks)custom_components/chefkoch_ha/const.py
(1 hunks)custom_components/chefkoch_ha/strings.json
(1 hunks)custom_components/chefkoch_ha/translations/de.json
(1 hunks)custom_components/chefkoch_ha/translations/en.json
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
custom_components/chefkoch_ha/__init__.py (1)
custom_components/chefkoch_ha/sensor.py (2)
sensor_id
(41-43)async_setup_entry
(9-19)
🪛 Ruff (0.13.1)
custom_components/chefkoch_ha/config_flow.py
43-43: Unused method argument: user_input
(ARG002)
53-53: Consider [*self.current_sensors, new_sensor]
instead of concatenation
Replace with [*self.current_sensors, new_sensor]
(RUF005)
custom_components/chefkoch_ha/__init__.py
91-91: Do not catch blind exception: Exception
(BLE001)
92-92: Use logging.exception
instead of logging.error
Replace with exception
(TRY400)
98-98: Do not catch blind exception: Exception
(BLE001)
🪛 GitHub Actions: Validate with hassfest
custom_components/chefkoch_ha/strings.json
[error] 1-1: Invalid strings.json: extra keys not allowed @ data['options']['step']['init']['menu']. Got {'add_sensor': 'Add a new Search Sensor', 'edit_sensor': 'Edit an existing Search Sensor', 'remove_sensor': 'Remove a Search Sensor'} (hassfest). Command: docker run --rm -v /home/runner/work/ha-chefkoch/ha-chefkoch://github/workspace ghcr.io/home-assistant/hassfest
🪛 GitHub Actions: Cron actions
custom_components/chefkoch_ha/strings.json
[error] 1-1: Invalid strings.json: extra keys not allowed @ data['options']['step']['init']['menu']. Got {'add_sensor': 'Add a new Search Sensor', 'edit_sensor': 'Edit an existing Search Sensor', 'remove_sensor': 'Remove a Search Sensor'}
🔇 Additional comments (4)
custom_components/chefkoch_ha/__init__.py (1)
15-39
: Per‑sensor concurrent fetch pipeline LGTMThe overall structure (read sensors from options, per-sensor task, run in executor/to_thread, aggregate by sensor_id) is sound.
custom_components/chefkoch_ha/translations/en.json (1)
15-22
: Rename ‘menu’ to ‘menu_options’ to match strings schemaSame issue as strings.json; align to menu_options.
Apply this diff:
"init": { "title": "Manage Chefkoch Sensors", - "menu": { + "menu_options": { "add_sensor": "Add a new Search Sensor", "edit_sensor": "Edit an existing Search Sensor", "remove_sensor": "Remove a Search Sensor" } },custom_components/chefkoch_ha/translations/de.json (1)
15-22
: ‘menu’ → ‘menu_options’ (schema compliance)Mirror the base strings change in de.json.
Apply this diff:
"init": { "title": "Chefkoch-Sensoren verwalten", - "menu": { + "menu_options": { "add_sensor": "Neuen Such-Sensor hinzufügen", "edit_sensor": "Bestehenden Such-Sensor bearbeiten", "remove_sensor": "Such-Sensor entfernen" } },custom_components/chefkoch_ha/strings.json (1)
15-22
: Fix hassfest failure: rename ‘menu’ to ‘menu_options’Hassfest rejects options.step.init.menu. Use menu_options.
Apply this diff:
"init": { "title": "Manage Chefkoch Sensors", - "menu": { + "menu_options": { "add_sensor": "Add a new Search Sensor", "edit_sensor": "Edit an existing Search Sensor", "remove_sensor": "Remove a Search Sensor" } },
async def _fetch_recipe_url(sensor_config: dict) -> str | None: | ||
"""Fetch the recipe URL based on sensor config.""" | ||
sensor_type = sensor_config["type"] | ||
|
||
if sensor_type == "random": | ||
retriever = RandomRetriever() | ||
recipe = await asyncio.to_thread(retriever.get_recipe) | ||
return recipe.url if recipe else None | ||
|
||
elif sensor_type == "daily": | ||
retriever = DailyRecipeRetriever() | ||
recipes = await asyncio.to_thread(retriever.get_recipes, type="kochen") | ||
return recipes[0].url if recipes and recipes[0] else None | ||
|
||
elif sensor_type == "vegan": | ||
retriever = SearchRetriever(health=["Vegan"]) | ||
recipes = await asyncio.to_thread(retriever.get_recipes, search_query="vegan") | ||
return recipes[0].url if recipes and recipes[0] else None | ||
|
||
elif sensor_type == "search": | ||
search_query = sensor_config.get("search_query", "") | ||
|
||
# Helper to parse comma-separated strings into lists | ||
def parse_list(key): | ||
value = sensor_config.get(key, "") | ||
return [item.strip() for item in value.split(',') if item.strip()] if value else None | ||
|
||
init_params = { | ||
"properties": parse_list("properties"), | ||
"health": parse_list("health"), | ||
"categories": parse_list("categories"), | ||
"countries": parse_list("countries"), | ||
"meal_type": parse_list("meal_type"), | ||
"prep_times": sensor_config.get("prep_times"), | ||
"ratings": sensor_config.get("ratings"), | ||
"sort": sensor_config.get("sort") | ||
} | ||
|
||
init_params = {k: v for k, v in init_params.items() if v} | ||
|
||
retriever = SearchRetriever(**init_params) | ||
recipes = await asyncio.to_thread(retriever.get_recipes, search_query=search_query) | ||
return recipes[0].url if recipes and recipes[0] else None | ||
|
||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Make first-recipe URL extraction robust; don’t drop valid “0” values in filters
- Accessing recipes[0] is repeated and risks AttributeError if first item is None. Centralize safe extraction.
- Filtering with “if v” drops valid falsy values (e.g., 0). Use explicit checks.
Apply this diff:
async def _fetch_recipe_url(sensor_config: dict) -> str | None:
@@
- if sensor_type == "random":
+ if sensor_type == "random":
retriever = RandomRetriever()
recipe = await asyncio.to_thread(retriever.get_recipe)
return recipe.url if recipe else None
@@
- elif sensor_type == "daily":
+ elif sensor_type == "daily":
retriever = DailyRecipeRetriever()
recipes = await asyncio.to_thread(retriever.get_recipes, type="kochen")
- return recipes[0].url if recipes and recipes[0] else None
+ first = recipes[0] if recipes else None
+ return getattr(first, "url", None)
@@
- elif sensor_type == "vegan":
+ elif sensor_type == "vegan":
retriever = SearchRetriever(health=["Vegan"])
recipes = await asyncio.to_thread(retriever.get_recipes, search_query="vegan")
- return recipes[0].url if recipes and recipes[0] else None
+ first = recipes[0] if recipes else None
+ return getattr(first, "url", None)
@@
- elif sensor_type == "search":
+ elif sensor_type == "search":
search_query = sensor_config.get("search_query", "")
@@
- init_params = {k: v for k, v in init_params.items() if v}
+ # Keep zeros; drop only None and empty strings
+ init_params = {
+ k: v
+ for k, v in init_params.items()
+ if v is not None and (not isinstance(v, str) or v.strip() != "")
+ }
@@
- recipes = await asyncio.to_thread(retriever.get_recipes, search_query=search_query)
- return recipes[0].url if recipes and recipes[0] else None
+ recipes = await asyncio.to_thread(retriever.get_recipes, search_query=search_query)
+ first = recipes[0] if recipes else None
+ return getattr(first, "url", None)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async def _fetch_recipe_url(sensor_config: dict) -> str | None: | |
"""Fetch the recipe URL based on sensor config.""" | |
sensor_type = sensor_config["type"] | |
if sensor_type == "random": | |
retriever = RandomRetriever() | |
recipe = await asyncio.to_thread(retriever.get_recipe) | |
return recipe.url if recipe else None | |
elif sensor_type == "daily": | |
retriever = DailyRecipeRetriever() | |
recipes = await asyncio.to_thread(retriever.get_recipes, type="kochen") | |
return recipes[0].url if recipes and recipes[0] else None | |
elif sensor_type == "vegan": | |
retriever = SearchRetriever(health=["Vegan"]) | |
recipes = await asyncio.to_thread(retriever.get_recipes, search_query="vegan") | |
return recipes[0].url if recipes and recipes[0] else None | |
elif sensor_type == "search": | |
search_query = sensor_config.get("search_query", "") | |
# Helper to parse comma-separated strings into lists | |
def parse_list(key): | |
value = sensor_config.get(key, "") | |
return [item.strip() for item in value.split(',') if item.strip()] if value else None | |
init_params = { | |
"properties": parse_list("properties"), | |
"health": parse_list("health"), | |
"categories": parse_list("categories"), | |
"countries": parse_list("countries"), | |
"meal_type": parse_list("meal_type"), | |
"prep_times": sensor_config.get("prep_times"), | |
"ratings": sensor_config.get("ratings"), | |
"sort": sensor_config.get("sort") | |
} | |
init_params = {k: v for k, v in init_params.items() if v} | |
retriever = SearchRetriever(**init_params) | |
recipes = await asyncio.to_thread(retriever.get_recipes, search_query=search_query) | |
return recipes[0].url if recipes and recipes[0] else None | |
return None | |
async def _fetch_recipe_url(sensor_config: dict) -> str | None: | |
"""Fetch the recipe URL based on sensor config.""" | |
sensor_type = sensor_config["type"] | |
if sensor_type == "random": | |
retriever = RandomRetriever() | |
recipe = await asyncio.to_thread(retriever.get_recipe) | |
return recipe.url if recipe else None | |
elif sensor_type == "daily": | |
retriever = DailyRecipeRetriever() | |
recipes = await asyncio.to_thread(retriever.get_recipes, type="kochen") | |
first = recipes[0] if recipes else None | |
return getattr(first, "url", None) | |
elif sensor_type == "vegan": | |
retriever = SearchRetriever(health=["Vegan"]) | |
recipes = await asyncio.to_thread(retriever.get_recipes, search_query="vegan") | |
first = recipes[0] if recipes else None | |
return getattr(first, "url", None) | |
elif sensor_type == "search": | |
search_query = sensor_config.get("search_query", "") | |
# Helper to parse comma-separated strings into lists | |
def parse_list(key): | |
value = sensor_config.get(key, "") | |
return [item.strip() for item in value.split(',') if item.strip()] if value else None | |
init_params = { | |
"properties": parse_list("properties"), | |
"health": parse_list("health"), | |
"categories": parse_list("categories"), | |
"countries": parse_list("countries"), | |
"meal_type": parse_list("meal_type"), | |
"prep_times": sensor_config.get("prep_times"), | |
"ratings": sensor_config.get("ratings"), | |
"sort": sensor_config.get("sort") | |
} | |
# Keep zeros; drop only None and empty strings | |
init_params = { | |
k: v | |
for k, v in init_params.items() | |
if v is not None and (not isinstance(v, str) or v.strip() != "") | |
} | |
retriever = SearchRetriever(**init_params) | |
recipes = await asyncio.to_thread(retriever.get_recipes, search_query=search_query) | |
first = recipes[0] if recipes else None | |
return getattr(first, "url", None) | |
return None |
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/__init__.py around lines 41 to 85, centralize
safe extraction of the first recipe URL and avoid filtering out valid falsy
filter values: add a small helper (e.g., _first_recipe_url(recipes)) that
returns recipes[0].url only when recipes is a sequence with at least one element
and recipes[0] is not None, then replace repeated recipes[0].url checks with
calls to that helper; when constructing init_params use a filter that preserves
falsy-but-valid values (e.g., {k:v for k,v in init_params.items() if v is not
None} or explicitly check v is not None) instead of truthiness so 0 or
empty-string-as-valid are kept.
if self._async_current_entries(): | ||
return self.async_abort(reason="single_instance_allowed") | ||
if user_input is not None: | ||
return self.async_create_entry(title="Chefkoch", data={}, options={"sensors": DEFAULT_SENSORS}) | ||
return self.async_show_form(step_id="user") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deep-copy DEFAULT_SENSORS to avoid mutating the constant
Passing the list directly can leak mutations back into the module constant.
Apply this diff:
- return self.async_create_entry(title="Chefkoch", data={}, options={"sensors": DEFAULT_SENSORS})
+ return self.async_create_entry(
+ title="Chefkoch",
+ data={},
+ options={"sensors": copy.deepcopy(DEFAULT_SENSORS)},
+ )
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/config_flow.py around lines 26 to 30, the code
assigns the module constant DEFAULT_SENSORS directly into the created entry
options, which can allow downstream mutations to alter the constant; import the
copy module and use copy.deepcopy(DEFAULT_SENSORS) (or an equivalent deep-copy)
when building options, e.g. options={"sensors": copy.deepcopy(DEFAULT_SENSORS)},
and add the import at the top of the file so the constant isn’t mutated.
async def async_step_edit_sensor_form(self, user_input=None): | ||
sensor_to_edit = next((s for s in self.current_sensors if s["id"] == self.sensor_to_edit_id), None) | ||
if not sensor_to_edit: | ||
return self.async_abort(reason="no_sensors") | ||
if user_input is not None: | ||
sensor_to_edit.update(user_input) | ||
return self.async_create_entry(title="", data={"sensors": self.current_sensors}) | ||
return self.async_show_form(step_id="edit_sensor_form", data_schema=get_search_schema(sensor_to_edit), last_step=True) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Abort reason key mismatch (‘no_sensors’ not defined in translations)
Use the existing ‘no_custom_sensors’ abort key to avoid untranslated aborts.
Apply this diff:
- if not sensor_to_edit:
- return self.async_abort(reason="no_sensors")
+ if not sensor_to_edit:
+ return self.async_abort(reason="no_custom_sensors")
To confirm there’s no ‘no_sensors’ key in translations, run:
🏁 Script executed:
#!/bin/bash
rg -n -C1 -S '"no_sensors"' custom_components/chefkoch_ha/strings.json custom_components/chefkoch_ha/translations/*.json || true
Length of output: 0
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repo root: $(pwd)"
echo
echo "List custom_components/chefkoch_ha:"
ls -la custom_components/chefkoch_ha || true
echo
echo "Show config_flow.py (lines 1-200):"
sed -n '1,200p' custom_components/chefkoch_ha/config_flow.py || true
echo
echo "Search repo for abort keys (no_sensors / no_custom_sensors):"
rg -n --hidden -S '\bno_sensors\b' || true
rg -n --hidden -S '\bno_custom_sensors\b' || true
echo
echo "Show strings.json (if exists):"
sed -n '1,200p' custom_components/chefkoch_ha/strings.json || true
echo
echo "Show translation files (if any):"
for f in custom_components/chefkoch_ha/translations/*.json; do
echo "---- $f ----"
sed -n '1,200p' "$f" || true
done
Length of output: 12810
Replace undefined abort reason 'no_sensors' with existing 'no_custom_sensors'
strings.json and translations use "no_custom_sensors" — update the abort reason to match.
- if not sensor_to_edit:
- return self.async_abort(reason="no_sensors")
+ if not sensor_to_edit:
+ return self.async_abort(reason="no_custom_sensors")
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
async def async_step_edit_sensor_form(self, user_input=None): | |
sensor_to_edit = next((s for s in self.current_sensors if s["id"] == self.sensor_to_edit_id), None) | |
if not sensor_to_edit: | |
return self.async_abort(reason="no_sensors") | |
if user_input is not None: | |
sensor_to_edit.update(user_input) | |
return self.async_create_entry(title="", data={"sensors": self.current_sensors}) | |
return self.async_show_form(step_id="edit_sensor_form", data_schema=get_search_schema(sensor_to_edit), last_step=True) | |
async def async_step_edit_sensor_form(self, user_input=None): | |
sensor_to_edit = next((s for s in self.current_sensors if s["id"] == self.sensor_to_edit_id), None) | |
if not sensor_to_edit: | |
return self.async_abort(reason="no_custom_sensors") | |
if user_input is not None: | |
sensor_to_edit.update(user_input) | |
return self.async_create_entry(title="", data={"sensors": self.current_sensors}) | |
return self.async_show_form(step_id="edit_sensor_form", data_schema=get_search_schema(sensor_to_edit), last_step=True) |
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/config_flow.py around lines 66 to 74, the
async_step_edit_sensor_form aborts with an undefined reason "no_sensors"; change
that abort reason to the existing "no_custom_sensors" used in
strings/translations so the abort maps to the correct translation key; update
the return self.async_abort(reason="no_sensors") to use
reason="no_custom_sensors".
DEFAULT_SENSORS = [ | ||
{"type": "random", "id": "random", "name": "Chefkoch Random Recipe"}, | ||
{"type": "daily", "id": "daily", "name": "Chefkoch Daily Recipe"}, | ||
{"type": "vegan", "id": "vegan", "name": "Chefkoch Vegan Recipe"} | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Defaults look good; avoid accidental mutation of DEFAULT_SENSORS at runtime
When seeding entry.options, deep-copy this list to prevent later options edits from mutating the module constant via shared references.
I’ve proposed the concrete fix in config_flow.py to deep-copy when creating the entry.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Searching for DEFAULT_SENSORS occurrences..."
rg -n --hidden -S "DEFAULT_SENSORS" -g '!*__pycache__/*' -C3 || true
echo
echo "Searching for config_flow.py files..."
rg -n --hidden -S "config_flow.py" -g '!*__pycache__/*' || true
echo
echo "Show const.py (if present)..."
sed -n '1,200p' custom_components/chefkoch_ha/const.py || true
echo
echo "Show config_flow.py (if present)..."
sed -n '1,400p' custom_components/chefkoch_ha/config_flow.py || true
Length of output: 5386
Deep-copy DEFAULT_SENSORS before storing it in config entry options
Seeding entry.options with the module list stores it by reference; the options flow mutates that list (sensor_to_edit.update(...)) and will alter the module constant. Fix in custom_components/chefkoch_ha/config_flow.py (async_step_user): use copy.deepcopy(DEFAULT_SENSORS) when creating the entry options instead of passing DEFAULT_SENSORS directly.
🤖 Prompt for AI Agents
In custom_components/chefkoch_ha/const.py around lines 4 to 8 the
DEFAULT_SENSORS list is a module-level constant that is being passed by
reference into a config entry; in custom_components/chefkoch_ha/config_flow.py
(async_step_user) change the code that seeds entry.options to use
copy.deepcopy(DEFAULT_SENSORS) (and add an import for copy at the top if
missing) so the flow mutates a deep copy instead of the module constant,
preventing accidental modification of DEFAULT_SENSORS.
Proposed Changes
Full rewrite of the integration to support new sensors, more attributes and custom sensors with search queries.
Related Issues
Summary by CodeRabbit
New Features
Localization
Chores