Add Litter-Robot 5 night light#174508
Conversation
|
Hey there @natekspencer, @tkdrob, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
There was a problem hiding this comment.
Pull request overview
This PR adds night light control for the Litter-Robot 5 (LR5) to the platinum-quality litterrobot integration. It introduces a new light platform for the globe night light (RGB color, brightness, on/off) and a night_light_preset select offering seven fully-saturated preset colors, and it remaps the LR5 globe_brightness select to LR5-specific percentages to work around a documented firmware brightness quirk. It also fixes LitterRobotSelectEntity.current_option to return None instead of the literal string "None" when no option matches.
Changes:
- New
light.pyplatform (LitterRobotNightLight) exposing RGB/brightness/on-off, writing mode+brightness+color together because the device replaces the whole settings object per command. - New
night_light_presetselect and an LR5-specificglobe_brightnessmapping (low/medium/high→10/100/75); LR4 keeps itsBrightnessLevelenum. - Fix for
current_optionreturning"None", plus translations, icons,PLATFORMSregistration, test mock, and new/updated tests.
I verified that set_night_light_settings, night_light_color, and NightLightMode exist in the pinned pylitterbot==2025.4.0 (added in 2025.2.0), that LR5 is not a subclass of LR4 so no duplicate globe_brightness entity is produced, and that the mode-based is_on (vs. the library's is_night_light_on) is a deliberate choice that keeps AUTO mode consistent when toggling. Two design points warrant human attention: the new light overlaps with the existing globe_light/globe_brightness/night_light_preset selects (all controlling the same night-light setting), and the intentionally non-monotonic LR5 brightness mapping.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
homeassistant/components/litterrobot/light.py |
New LR5 night-light entity with RGB/brightness/on-off control. |
homeassistant/components/litterrobot/select.py |
Adds night_light_preset + LR5 globe_brightness; splits LR4/LR5; fixes current_option. |
homeassistant/components/litterrobot/__init__.py |
Registers the LIGHT platform. |
homeassistant/components/litterrobot/strings.json |
Adds night light and preset translations. |
homeassistant/components/litterrobot/icons.json |
Adds icons for the new entities. |
tests/components/litterrobot/conftest.py |
Mocks set_night_light_settings. |
tests/components/litterrobot/test_light.py |
New tests for the light platform. |
tests/components/litterrobot/test_select.py |
Tests for the preset select; LR5 globe_brightness remains untested. |
tests/components/litterrobot/snapshots/test_light.ambr |
Snapshot for the new light entity. |
| LitterRobot5: ( | ||
| RobotSelectEntityDescription[LitterRobot5, str]( | ||
| key="globe_brightness", | ||
| translation_key="globe_brightness", | ||
| current_fn=lambda robot: next( | ||
| ( | ||
| level | ||
| for level, pct in LR5_GLOBE_BRIGHTNESS.items() | ||
| if pct == robot.night_light_brightness | ||
| ), | ||
| None, | ||
| ), | ||
| options_fn=lambda _: list(LR5_GLOBE_BRIGHTNESS), | ||
| select_fn=lambda robot, opt: robot.set_night_light_brightness( | ||
| LR5_GLOBE_BRIGHTNESS[opt] | ||
| ), | ||
| ), |
| LitterRobot5: ( | ||
| RobotSelectEntityDescription[LitterRobot5, str]( | ||
| key="globe_brightness", | ||
| translation_key="globe_brightness", | ||
| current_fn=lambda robot: next( | ||
| ( | ||
| level | ||
| for level, pct in LR5_GLOBE_BRIGHTNESS.items() | ||
| if pct == robot.night_light_brightness | ||
| ), | ||
| None, | ||
| ), | ||
| options_fn=lambda _: list(LR5_GLOBE_BRIGHTNESS), | ||
| select_fn=lambda robot, opt: robot.set_night_light_brightness( | ||
| LR5_GLOBE_BRIGHTNESS[opt] | ||
| ), | ||
| ), |
e641363 to
f67a183
Compare
| # Fully saturated colors (every channel 0 or 255) are the only ones the LED | ||
| # renders accurately at all brightness levels; intermediate colors drift. They | ||
| # are offered as named presets while the light's RGB picker stays available for | ||
| # custom colors. | ||
| NIGHT_LIGHT_PRESETS: dict[str, str] = { | ||
| "red": "#FF0000", | ||
| "green": "#00FF00", | ||
| "blue": "#0000FF", | ||
| "cyan": "#00FFFF", | ||
| "magenta": "#FF00FF", | ||
| "yellow": "#FFFF00", | ||
| "white": "#FFFFFF", | ||
| } |
There was a problem hiding this comment.
Do we really want to make presets if we have all the colors we want?
There was a problem hiding this comment.
Yes — I'd argue we do, because on this hardware "all the colors we want" is exactly what we don't reliably have. The rendered color for a stored RGB value isn't necessarily the color the globe shows: it varies with the brightness level. This is reproducible in the stock Whisker app (which only offers a color wheel and brightness slider — no hex or numeric input): pick a color, change the brightness, and the globe can render a visibly different hue.
Concrete example, live on my LR5 Pro right now — night light set to #83FF5B / RGB (131, 255, 91), spring green, with the stored value verified identical in the app, the cloud API, pylitterbot, and HA. Varying only brightness:
| Brightness | Globe shows |
|---|---|
| 10–30% | green (faithful) |
| 40–50% | vivid purple (~170, 60, 220) |
| 60–75% | near-white with a slight purple tint |
| 100% | light blue (~140, 190, 255) |
Same stored color, four distinct renderings, faithful only below ~⅓ brightness — and deterministic: the same values render the same way whether set via the brightness slider or the select. Since the data path carries the value unchanged end-to-end, this is the device's rendering, not the integration or library.
The seven presets are colors verified on hardware (all seven re-tested today) to render true at every brightness level. The light entity still provides the full color wheel and brightness slider, so users can absolutely find their own colors — the presets just give them a set that's proven to display correctly. Happy to attach photos of the globe showing the same stored color at 20% (green) vs 44% (purple).
| def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]: | ||
| """Convert a color hex string into an RGB tuple.""" | ||
| value = hex_color.lstrip("#") | ||
| if len(value) == 3: | ||
| value = "".join(component * 2 for component in value) | ||
| return int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16) |
There was a problem hiding this comment.
IMO, hex to RGB should be in the library (if it's not a common helper already)
There was a problem hiding this comment.
Agreed — opened natekspencer/pylitterbot#425 adding a night_light_rgb_color property (and RGB tuple support in set_night_light_settings). Once it's released I'll bump the requirement here and drop the local helper.
There was a problem hiding this comment.
Done in cec15d4 — pylitterbot 2025.6.1 (which includes natekspencer/pylitterbot#425) is now required, the local _hex_to_rgb helper is gone, the light entity reads night_light_rgb_color and passes RGB tuples to set_night_light_settings, and the presets are plain RGB tuples. No color hex knowledge left in the integration.
| # The LR5 globe LED renders brightness non-monotonically (a firmware quirk also | ||
| # present in the Whisker app), so LOW/MEDIUM/HIGH map to the percentages that | ||
| # read as dim/medium/bright on the LR5 rather than the LR4 25/50/100 levels. The | ||
| # light entity exposes the full continuous range. | ||
| LR5_GLOBE_BRIGHTNESS: dict[str, int] = {"low": 10, "medium": 100, "high": 75} |
There was a problem hiding this comment.
Is this the same as the light brightness?
There was a problem hiding this comment.
It writes the same underlying device setting, yes — but it isn't a duplicate of the slider, it's a calibrated view of it, because the firmware doesn't render the numeric scale faithfully. Brightness is non-monotonic on the LR5: 75% displays visibly brighter than 100%. Anyone with the device can reproduce this with pylitterbot alone (set_night_light_brightness(100), look at the globe, then (75)) — so it's the device, not this integration. That's why low/medium/high map to eye-calibrated percentages (10/100/75) that actually read as dim/medium/bright on the hardware, while the light entity's slider keeps the full range. The LR4 keeps its standard 25/50/100 levels.
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
| LitterRobot5: ( | ||
| RobotSelectEntityDescription[LitterRobot5, str]( | ||
| key="globe_brightness", | ||
| translation_key="globe_brightness", | ||
| current_fn=lambda robot: next( | ||
| ( | ||
| level | ||
| for level, pct in LR5_GLOBE_BRIGHTNESS.items() | ||
| if pct == robot.night_light_brightness | ||
| ), | ||
| None, | ||
| ), | ||
| options_fn=lambda _: list(LR5_GLOBE_BRIGHTNESS), | ||
| select_fn=lambda robot, opt: robot.set_night_light_brightness( | ||
| LR5_GLOBE_BRIGHTNESS[opt] | ||
| ), | ||
| ), | ||
| RobotSelectEntityDescription[LitterRobot5, str]( | ||
| key="night_light_preset", | ||
| translation_key="night_light_preset", | ||
| current_fn=_active_night_light_preset, | ||
| options_fn=lambda _: list(NIGHT_LIGHT_PRESETS), | ||
| select_fn=lambda robot, opt: robot.set_night_light_settings( | ||
| # The API replaces the whole object, so keep mode + brightness. | ||
| mode=robot.night_light_mode or NightLightMode.ON, | ||
| brightness=robot.night_light_brightness, | ||
| color=NIGHT_LIGHT_PRESETS[opt], | ||
| ), | ||
| ), |
Adds an RGB light entity for the Litter-Robot 5 globe night light, plus a globe brightness and night light preset select. Includes @OverRide markers required by current mypy and tests for the LR5 globe brightness mapping. Rebased on dev; regenerated the light snapshot for the new enum-keyed capability/state attribute serialization.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump pylitterbot to 2025.6.1 and drop the local hex parsing: the light entity reads night_light_rgb_color and passes RGB tuples straight to set_night_light_settings, and the night light presets are defined as RGB tuples, removing all color hex knowledge from the integration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17c2b63 to
cec15d4
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Breaking change
None.
Proposed change
Adds night light support for the Litter-Robot 5 (LR5 Pro):
lightentity for the globe night light, exposing RGB color,brightness, and on/off. Color, brightness, and mode are written together
because the device replaces the entire night light settings object on each
command.
night_light_presetselect offering the seven fully-saturatedcolors (red, green, blue, cyan, magenta, yellow, white) that the LED renders
accurately at every brightness. Intermediate colors drift with brightness, so
the light's RGB picker remains available for custom colors. Selecting a preset
preserves the current mode and brightness.
globe_brightnessselect's LOW/MEDIUM/HIGH levels are mapped toLR5-specific percentages (10/100/75). The LR5 globe LED renders brightness
non-monotonically — a firmware quirk also present in the Whisker app, where
e.g. 25% can appear brighter than 100% — so these values were chosen so the
levels read as dim/medium/bright on the LR5. The Litter-Robot 4 keeps the
standard 25/50/100 levels.
It also fixes
LitterRobotSelectEntity.current_optionto returnNone(Unknown) instead of the literal string
"None"when the current value matchesno option.
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: