Skip to content

Commit 0e1abdc

Browse files
author
Tom Lasswell
committed
fix: Use correct API capability types for heater temperature and fan speed (#13)
Temperature control was sending devices.capabilities.range/temperature (plain integer) but heaters require devices.capabilities.temperature_setting/ targetTemperature with a STRUCT value {"temperature": N, "unit": "Celsius"}. Fan speed looked for a non-existent devices.capabilities.mode/fanSpeed capability instead of reading workMode options from devices.capabilities.work_mode.
1 parent 9873602 commit 0e1abdc

File tree

8 files changed

+127
-54
lines changed

8 files changed

+127
-54
lines changed

custom_components/govee/coordinator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
MusicModeCommand,
4141
PowerCommand,
4242
SceneCommand,
43+
TemperatureSettingCommand,
4344
ToggleCommand,
4445
create_dreamview_command,
4546
)
@@ -883,6 +884,8 @@ def _apply_optimistic_update(
883884
elif isinstance(command, ModeCommand):
884885
if command.mode_instance == INSTANCE_HDMI_SOURCE:
885886
state.apply_optimistic_hdmi_source(command.value)
887+
elif isinstance(command, TemperatureSettingCommand):
888+
state.heater_temperature = command.temperature
886889
elif isinstance(command, MusicModeCommand):
887890
# Look up mode name from device capabilities for display
888891
device = self._devices.get(device_id)

custom_components/govee/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"cryptography>=41.0.0"
1717
],
1818
"ssdp": [],
19-
"version": "2026.2.7",
19+
"version": "2026.2.8",
2020
"zeroconf": []
2121
}

custom_components/govee/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
RangeCommand,
1717
SceneCommand,
1818
SegmentColorCommand,
19+
TemperatureSettingCommand,
1920
ToggleCommand,
2021
WorkModeCommand,
2122
create_dreamview_command,
@@ -54,6 +55,7 @@
5455
"WorkModeCommand",
5556
"ModeCommand",
5657
"MusicModeCommand",
58+
"TemperatureSettingCommand",
5759
"create_dreamview_command",
5860
"create_night_light_command",
5961
]

custom_components/govee/models/commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CAPABILITY_ON_OFF,
1919
CAPABILITY_RANGE,
2020
CAPABILITY_SEGMENT_COLOR,
21+
CAPABILITY_TEMPERATURE_SETTING,
2122
CAPABILITY_TOGGLE,
2223
CAPABILITY_WORK_MODE,
2324
INSTANCE_BRIGHTNESS,
@@ -31,6 +32,7 @@
3132
INSTANCE_POWER,
3233
INSTANCE_SCENE,
3334
INSTANCE_SEGMENT_COLOR,
35+
INSTANCE_TARGET_TEMPERATURE,
3436
INSTANCE_WORK_MODE,
3537
)
3638
from .state import RGBColor
@@ -381,3 +383,26 @@ def get_value(self) -> dict[str, Any]:
381383
if self.rgb is not None and self.auto_color == 0:
382384
value["rgb"] = self.rgb
383385
return value
386+
387+
388+
@dataclass(frozen=True)
389+
class TemperatureSettingCommand(DeviceCommand):
390+
"""Command to set heater target temperature via STRUCT payload.
391+
392+
Heaters use the temperature_setting capability with a STRUCT value
393+
containing temperature and unit fields.
394+
"""
395+
396+
temperature: int
397+
unit: str = "Celsius"
398+
399+
@property
400+
def capability_type(self) -> str:
401+
return CAPABILITY_TEMPERATURE_SETTING
402+
403+
@property
404+
def instance(self) -> str:
405+
return INSTANCE_TARGET_TEMPERATURE
406+
407+
def get_value(self) -> dict[str, Any]:
408+
return {"temperature": self.temperature, "unit": self.unit}

custom_components/govee/models/device.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
CAPABILITY_WORK_MODE = "devices.capabilities.work_mode"
2323
CAPABILITY_PROPERTY = "devices.capabilities.property"
2424
CAPABILITY_MODE = "devices.capabilities.mode"
25+
CAPABILITY_TEMPERATURE_SETTING = "devices.capabilities.temperature_setting"
2526

2627
# Device type constants
2728
DEVICE_TYPE_LIGHT = "devices.types.light"
@@ -48,6 +49,7 @@
4849
INSTANCE_MUSIC_MODE = "musicMode"
4950
INSTANCE_DREAMVIEW = "dreamViewToggle"
5051
INSTANCE_TEMPERATURE = "temperature"
52+
INSTANCE_TARGET_TEMPERATURE = "targetTemperature"
5153
INSTANCE_FAN_SPEED = "fanSpeed"
5254
INSTANCE_PURIFIER_MODE = "purifierMode"
5355

@@ -381,26 +383,39 @@ def get_music_sensitivity_range(self) -> tuple[int, int]:
381383
def get_temperature_range(self) -> tuple[int, int]:
382384
"""Extract temperature range from capability.
383385
386+
Parses STRUCT-based temperature_setting capability where the range
387+
is nested inside the fields array under the 'temperature' field.
388+
384389
Returns (min, max) tuple, defaulting to (16, 35) Celsius.
385390
"""
386391
for cap in self.capabilities:
387-
if cap.type == CAPABILITY_RANGE and cap.instance == INSTANCE_TEMPERATURE:
388-
range_data = cap.parameters.get("range", {})
389-
return (
390-
int(range_data.get("min", 16)),
391-
int(range_data.get("max", 35)),
392-
)
392+
if (
393+
cap.type == CAPABILITY_TEMPERATURE_SETTING
394+
and cap.instance == INSTANCE_TARGET_TEMPERATURE
395+
):
396+
for f in cap.parameters.get("fields", []):
397+
if f.get("fieldName") == "temperature":
398+
range_data = f.get("range", {})
399+
return (
400+
int(range_data.get("min", 16)),
401+
int(range_data.get("max", 35)),
402+
)
393403
return (16, 35)
394404

395405
def get_fan_speed_options(self) -> list[dict[str, Any]]:
396-
"""Extract fan speed mode options from capability.
406+
"""Extract fan speed options from work_mode capability.
407+
408+
Heater fan speed options are inside the STRUCT-based work_mode
409+
capability, in the 'workMode' field's options array.
397410
398411
Returns list of {"name": "Low", "value": 1} dicts.
399412
"""
400413
for cap in self.capabilities:
401-
if cap.type == CAPABILITY_MODE and cap.instance == INSTANCE_FAN_SPEED:
402-
options: list[dict[str, Any]] = cap.parameters.get("options", [])
403-
return options
414+
if cap.type == CAPABILITY_WORK_MODE and cap.instance == INSTANCE_WORK_MODE:
415+
for f in cap.parameters.get("fields", []):
416+
if f.get("fieldName") == "workMode":
417+
options: list[dict[str, Any]] = f.get("options", [])
418+
return options
404419
return []
405420

406421
def get_purifier_mode_options(self) -> list[dict[str, Any]]:

custom_components/govee/number.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from .const import DOMAIN, SUFFIX_HEATER_TEMPERATURE, SUFFIX_MUSIC_SENSITIVITY
2020
from .coordinator import GoveeCoordinator
21-
from .models import GoveeDevice, MusicModeCommand, RangeCommand
21+
from .models import GoveeDevice, MusicModeCommand, TemperatureSettingCommand
2222

2323
_LOGGER = logging.getLogger(__name__)
2424

@@ -306,9 +306,8 @@ async def async_set_native_value(self, value: float) -> None:
306306
"""
307307
temperature = int(value)
308308

309-
command = RangeCommand(
310-
range_instance="temperature",
311-
value=temperature,
309+
command = TemperatureSettingCommand(
310+
temperature=temperature,
312311
)
313312

314313
success = await self.coordinator.async_control_device(

custom_components/govee/select.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
ModeCommand,
3737
MusicModeCommand,
3838
SceneCommand,
39+
WorkModeCommand,
3940
)
40-
from .models.device import INSTANCE_FAN_SPEED, INSTANCE_HDMI_SOURCE, INSTANCE_PURIFIER_MODE
41+
from .models.device import INSTANCE_HDMI_SOURCE, INSTANCE_PURIFIER_MODE
4142

4243
# DIY Style options for select entity
4344
DIY_STYLE_OPTIONS = list(DIY_STYLE_NAMES.keys())
@@ -773,10 +774,9 @@ def __init__(
773774
def current_option(self) -> str | None:
774775
"""Return current selected option from state."""
775776
state = self.coordinator.get_state(self._device_id)
776-
if state and state.fan_speed is not None:
777-
# Find option name matching the current value
777+
if state and state.work_mode is not None:
778778
for name, value in self._option_map.items():
779-
if value == state.fan_speed:
779+
if value == state.work_mode:
780780
return name
781781
# Return first option as default if available
782782
return self._attr_options[0] if self._attr_options else None
@@ -788,9 +788,9 @@ async def async_select_option(self, option: str) -> None:
788788
_LOGGER.warning("Unknown fan speed option: %s", option)
789789
return
790790

791-
command = ModeCommand(
792-
mode_instance=INSTANCE_FAN_SPEED,
793-
value=value,
791+
command = WorkModeCommand(
792+
work_mode=value,
793+
mode_value=0,
794794
)
795795

796796
success = await self.coordinator.async_control_device(

0 commit comments

Comments
 (0)