From 02f3adf27e71b1c1a07aaeadb8c4ca77c651f14a Mon Sep 17 00:00:00 2001 From: Sean Rees <sean@erifax.org> Date: Tue, 12 Jan 2021 09:31:24 +0000 Subject: [PATCH] Handle oscillation and oscillation_state differently on V2 Thanks to @Scaredycrow for finding this issue. It appears that V2 devices emit two states: oscillation (the configured mode; this is the intent) and oscillation_state (what the fan is currently doing). In most cases, these should be equivalent -- though the fan will report "IDLE" if the fan is intended to oscillate but has met its air quality/heat target & is thusly turned off. To make this work: dyson_oscillation_mode now reflects the intended mode (which is no change for V1 and not a significant change for V2), and a new metric: dyson_oscillation_state covers the possibility of IDLE. I also added code for V1 to simulate the V2 behaviour for consistency between the model generations. --- README.md | 3 ++- metrics.py | 45 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0b0e0d9..50878c6 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ dyson_fan_power | enum | all | ON if the fan is powered on, OFF otherwise dyson_auto_mode | enum | all | ON if the fan is in auto mode, OFF otherwise dyson_fan_state | enum | all | FAN, OFF (what the fan is actually doing) dyson_fan_speed_units | gauge | all | 0-10 (or -1 if on AUTO) -dyson_oscillation_mode | enum | all | ON if the fan is oscillating, OFF otherwise +dyson_oscillation_mode | enum | all | ON if the fan in oscillation mode, OFF otherwise +dyson_oscillation_state | enum | all | ON, OFF, IDLE. ON means the fan is currently oscillating, IDLE means the fan is in auto mode and the fan is paused dyson_oscillation_angle_low_degrees | gauge | V2 fans only | low angle of oscillation in degrees dyson_oscillation_angle_high_degrees | gauge | V2 fans only | high angle of oscillation in degrees dyson_night_mode | enum | all | ON if the fan is in night mode, OFF otherwise diff --git a/metrics.py b/metrics.py index 8d3f997..7b1e733 100644 --- a/metrics.py +++ b/metrics.py @@ -1,5 +1,6 @@ """Creates and maintains Prometheus metric values.""" +import enum import logging from libpurecool import const, dyson_pure_state, dyson_pure_state_v2 @@ -24,6 +25,13 @@ def update_enum(enum, name: str, serial: str, state): enum.labels(name=name, serial=serial).state(state) +class _OscillationState(enum.Enum): + """On V2 devices, oscillation_status can return 'IDLE' in auto mode.""" + ON = 'ON' + OFF = 'OFF' + IDLE = 'IDLE' + + class Metrics: """Registers/exports and updates Prometheus metrics for DysonLink fans.""" @@ -71,7 +79,9 @@ class Metrics: self.fan_speed = make_gauge( 'dyson_fan_speed_units', 'Current speed of fan (-1 = AUTO)') self.oscillation = make_enum( - 'dyson_oscillation_mode', 'Current oscillation mode', const.Oscillation) + 'dyson_oscillation_mode', 'Current oscillation mode (will the fan move?)', const.Oscillation) + self.oscillation_state = make_enum( + 'dyson_oscillation_state', 'Current oscillation state (is the fan moving?)', _OscillationState) self.night_mode = make_enum( 'dyson_night_mode', 'Night mode', const.NightMode) self.heat_mode = make_enum( @@ -188,7 +198,6 @@ class Metrics: self.updatePureCoolStateCommon(name, serial, message) update_enum(self.fan_mode, name, serial, message.fan_mode) - update_enum(self.oscillation, name, serial, message.oscillation) update_gauge(self.quality_target, name, serial, message.quality_target) @@ -203,6 +212,14 @@ class Metrics: update_enum(self.auto_mode, name, serial, auto) update_enum(self.fan_power, name, serial, power) + oscillation_state = message.oscillation + if message.fan_mode == const.FanMode.AUTO.value and message.fan_state == const.FanState.FAN_OFF: + # Compatibility with V2's behaviour for this value. + oscillation_state = _OscillationState.IDLE.value + + update_enum(self.oscillation, name, serial, message.oscillation) + update_enum(self.oscillation_state, name, serial, oscillation_state) + # Convert filter_life from hours to seconds. filter_life = int(message.filter_life) * 60 * 60 update_gauge(self.filter_life, name, serial, filter_life) @@ -228,10 +245,26 @@ class Metrics: update_gauge(self.night_mode_speed, name, serial, int(message.night_mode_speed)) - # V2 provides oscillation_status and oscillation as fields, - # oscillation_status provides values compatible with V1, so we use that. - # oscillation returns as 'OION', 'OIOF.' - update_enum(self.oscillation, name, serial, + # V2 devices differentiate between _current_ oscillation status ('on', 'off', 'idle') + # and configured mode ('on', 'off'). This is roughly the difference between + # "is it oscillating right now" (oscillation_status) and "will it oscillate" (oscillation). + # + # This issue https://github.com/etheralm/libpurecool/issues/4#issuecomment-563358021 + # seems to indicate that oscillation can be one of the OscillationV2 values (OION, OIOF) + # or one of the Oscillation values (ON, OFF) -- so support and translate both. + v2_to_v1_map = { + const.OscillationV2.OSCILLATION_ON.value: const.Oscillation.OSCILLATION_ON.value, + const.OscillationV2.OSCILLATION_OFF.value: const.Oscillation.OSCILLATION_OFF.value, + const.Oscillation.OSCILLATION_ON.value: const.Oscillation.OSCILLATION_ON.value, + const.Oscillation.OSCILLATION_OFF.value: const.Oscillation.OSCILLATION_OFF.value + } + oscillation = v2_to_v1_map.get(message.oscillation, None) + if oscillation: + update_enum(self.oscillation, name, serial, oscillation) + else: + logging.warning('Received unknown oscillation setting from "%s" (serial=%s): %s; ignoring', + name, serial, message.oscillation) + update_enum(self.oscillation_state, name, serial, message.oscillation_status) update_gauge(self.oscillation_angle_low, name, serial, int(message.oscillation_angle_low)) -- GitLab