diff --git a/README.md b/README.md index 00461c59aef5b63a4480ccb518c2ab35f4b2cfb3..1cfc2906b34ae957c89044b8c8b56ab3b57d6c6c 100644 --- a/README.md +++ b/README.md @@ -4,37 +4,51 @@ Prometheus client for DysonLink fans (e.g; Pure Hot+Cool). This code only supports Pure Hot+Cool fans at the moment. It should be trivial to extend to other fan types (I just don't have one to test). -## Dependencies +## Build + +``` +% bazel build :main +``` + +If you'd like a Debian package: +``` +% baze build :main-deb +``` + +### Without Bazel + +You'll need these dependencies: ``` pip install libpurecool pip install prometheus_client ``` + ## Metrics ### Environmental Name | Type | Description ---- | ---- | ----------- -humidity | gauge | relative humidity percentage -temperature | gauge | ambient temperature in celsius -voc | gauge | volatile organic compounds (range 0-10?) -dust | gauge | dust level (range 0-10?) +dyson_humidity_percent | gauge | relative humidity percentage +dyson_temperature_celsius | gauge | ambient temperature in celsius +dyson_volatile_organic_compounds_units | gauge | volatile organic compounds (range 0-10?) +dyson_dust_units | gauge | dust level (range 0-10?) ### Operational Name | Type | Description ---- | ---- | ----------- -fan_mode | enum | AUTO, FAN (what the fan is set to) -fan_state | enum | FAN, OFF (what the fan is actually doing) -fan_speed | gauge | 0-10 (or -1 if on AUTO) -oscillation | enum | ON, OFF -focus_mode | enum | ON, OFF -heat_mode | enum | HEAT, OFF (OFF means "in cooling mode") -heat_state | enum | HEAT, OFF (what the fan is actually doing) -heat_target | gauge | target temperature (celsius) -quality_target | gauge | air quality target (1, 3, 5?) -filter_life | gauge | hours of filter life remaining +dyson_fan_mode | enum | AUTO, FAN (what the fan is set to) +dyson_fan_state | enum | FAN, OFF (what the fan is actually doing) +dyson_fan_speed_units | gauge | 0-10 (or -1 if on AUTO) +dyson_oscillation_mode | enum | ON, OFF +dyson_focus_mode | enum | ON, OFF +dyson_heat_mode | enum | HEAT, OFF (OFF means "in cooling mode") +dyson_heat_state | enum | HEAT, OFF (what the fan is actually doing) +dyson_heat_target_celsius | gauge | target temperature (celsius) +dyson_quality_target_units | gauge | air quality target (1, 3, 5?) +dyson_filter_life_seconds | gauge | seconds of filter life remaining ## Usage diff --git a/grafana.json b/grafana.json index 31226cd9dfa0af1f259052470ec1670e5f2ab641..c5d6f042d5f76b26646990d5738627a2fcb160ef 100644 --- a/grafana.json +++ b/grafana.json @@ -52,7 +52,7 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1543741240899, + "iteration": 1600325678809, "links": [], "panels": [ { @@ -129,11 +129,12 @@ "tableColumn": "", "targets": [ { - "expr": "fan_mode{instance=~\"$instance\",name=~\"$name\"}==1", + "expr": "dyson_fan_mode{instance=~\"$instance\",name=~\"$name\"}==1", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, - "legendFormat": "{{fan_mode}}", + "legendFormat": "{{dyson_fan_mode}}", "refId": "A" } ], @@ -211,11 +212,12 @@ "tableColumn": "", "targets": [ { - "expr": "fan_state{instance=~\"$instance\",name=~\"$name\"}==1", + "expr": "dyson_fan_state{instance=~\"$instance\",name=~\"$name\"}==1", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, - "legendFormat": "{{fan_state}}", + "legendFormat": "{{dyson_fan_state}}", "refId": "A" } ], @@ -293,11 +295,12 @@ "tableColumn": "", "targets": [ { - "expr": "oscillation{instance=~\"$instance\",name=~\"$name\"}==1", + "expr": "dyson_oscillation_mode{instance=~\"$instance\",name=~\"$name\"}==1", "format": "time_series", "instant": true, + "interval": "", "intervalFactor": 1, - "legendFormat": "{{oscillation}}", + "legendFormat": "{{dyson_oscillation_mode}}", "refId": "A" } ], @@ -375,12 +378,12 @@ "tableColumn": "", "targets": [ { - "expr": "focus_mode{instance=~\"$instance\",name=~\"$name\"}==1", + "expr": "dyson_focus_mode{instance=~\"$instance\",name=~\"$name\"}==1", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, - "legendFormat": "{{focus_mode}}", + "legendFormat": "{{dyson_focus_mode}}", "refId": "A" } ], @@ -458,11 +461,11 @@ "tableColumn": "", "targets": [ { - "expr": "heat_mode{instance=~\"$instance\",name=~\"$name\"}==1", + "expr": "dyson_heat_mode{instance=~\"$instance\",name=~\"$name\"}==1", "format": "time_series", "instant": true, "intervalFactor": 1, - "legendFormat": "{{heat_mode}}", + "legendFormat": "{{dyson_heat_mode}}", "refId": "A" } ], @@ -571,7 +574,7 @@ "#d44a3a" ], "datasource": "${DS_LOCAL}", - "format": "h", + "format": "s", "gauge": { "maxValue": 100, "minValue": 0, @@ -622,7 +625,7 @@ "tableColumn": "", "targets": [ { - "expr": "filter_life{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_filter_life_seconds{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" @@ -694,7 +697,7 @@ "steppedLine": false, "targets": [ { - "expr": "dust{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_dust_units{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -702,14 +705,14 @@ "refId": "A" }, { - "expr": "voc{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_volatile_organic_compounds_units{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "VOC", "refId": "B" }, { - "expr": "quality_target{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_quality_target_units{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -797,7 +800,7 @@ "steppedLine": false, "targets": [ { - "expr": "humidity{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_humidity_percent{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 10, "legendFormat": "Humidity (%age)", @@ -883,14 +886,14 @@ "steppedLine": false, "targets": [ { - "expr": "temperature{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_temperature_celsius{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 10, "legendFormat": "Temperature", "refId": "A" }, { - "expr": "heat_target{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_heat_target_celsius{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Heat Target", @@ -920,7 +923,7 @@ "label": null, "logBase": 1, "max": null, - "min": null, + "min": "15", "show": true }, { @@ -987,9 +990,10 @@ "steppedLine": false, "targets": [ { - "expr": "filter_life{instance=~\"$instance\",name=~\"$name\"}", + "expr": "dyson_filter_life_seconds{instance=~\"$instance\",name=~\"$name\"}", "format": "time_series", "intervalFactor": 1, + "legendFormat": "Remaining Life - {{name}}", "refId": "A" } ], @@ -1012,7 +1016,7 @@ }, "yaxes": [ { - "format": "h", + "format": "s", "label": null, "logBase": 1, "max": null, @@ -1116,5 +1120,5 @@ "timezone": "", "title": "Dyson Fan", "uid": "wSsTjyYik", - "version": 7 + "version": 10 } \ No newline at end of file diff --git a/main.py b/main.py index 78785489274ad4726ab6afb15e4a9ed3508019db..b40e75df45963aa6e10777a1da2835d7434a9b9e 100755 --- a/main.py +++ b/main.py @@ -35,34 +35,37 @@ class Metrics(): labels = ['name', 'serial'] # Environmental Sensors - self.humidity = prometheus_client.Gauge('humidity', 'Relative humidity (percentage)', labels) + self.humidity = prometheus_client.Gauge( + 'dyson_humidity_percent', 'Relative humidity (percentage)', labels) self.temperature = prometheus_client.Gauge( - 'temperature', 'Ambient temperature (celsius)', labels) - self.voc = prometheus_client.Gauge('voc', 'Level of Volatile organic compounds', labels) - self.dust = prometheus_client.Gauge('dust', 'Level of Dust', labels) + 'dyson_temperature_celsius', 'Ambient temperature (celsius)', labels) + self.voc = prometheus_client.Gauge( + 'dyson_volatile_organic_compounds_units', 'Level of Volatile organic compounds', labels) + self.dust = prometheus_client.Gauge( + 'dyson_dust_units', 'Level of Dust', labels) # Operational State # Ignoring: tilt (known values OK), standby_monitoring. self.fan_mode = prometheus_client.Enum( - 'fan_mode', 'Current mode of the fan', labels, states=['AUTO', 'FAN']) + 'dyson_fan_mode', 'Current mode of the fan', labels, states=['AUTO', 'FAN']) self.fan_state = prometheus_client.Enum( - 'fan_state', 'Current running state of the fan', labels, states=['FAN', 'OFF']) + 'dyson_fan_state', 'Current running state of the fan', labels, states=['FAN', 'OFF']) self.fan_speed = prometheus_client.Gauge( - 'fan_speed', 'Current speed of fan (-1 = AUTO)', labels) + 'dyson_fan_speed_units', 'Current speed of fan (-1 = AUTO)', labels) self.oscillation = prometheus_client.Enum( - 'oscillation', 'Current oscillation mode', labels, states=['ON', 'OFF']) + 'dyson_oscillation_mode', 'Current oscillation mode', labels, states=['ON', 'OFF']) self.focus_mode = prometheus_client.Enum( - 'focus_mode', 'Current focus mode', labels, states=['ON', 'OFF']) + 'dyson_focus_mode', 'Current focus mode', labels, states=['ON', 'OFF']) self.heat_mode = prometheus_client.Enum( - 'heat_mode', 'Current heat mode', labels, states=['HEAT', 'OFF']) + 'dyson_heat_mode', 'Current heat mode', labels, states=['HEAT', 'OFF']) self.heat_state = prometheus_client.Enum( - 'heat_state', 'Current heat state', labels, states=['HEAT', 'OFF']) + 'dyson_heat_state', 'Current heat state', labels, states=['HEAT', 'OFF']) self.heat_target = prometheus_client.Gauge( - 'heat_target', 'Heat target temperature (celsius)', labels) + 'dyson_heat_target_celsius', 'Heat target temperature (celsius)', labels) self.quality_target = prometheus_client.Gauge( - 'quality_target', 'Quality target for fan', labels) + 'dyson_quality_target_units', 'Quality target for fan', labels) self.filter_life = prometheus_client.Gauge( - 'filter_life', 'Remaining filter life (hours)', labels) + 'dyson_filter_life_seconds', 'Remaining filter life (seconds)', labels) def update(self, name: str, serial: str, message: object) -> None: """Receives a sensor or device state update and updates Prometheus metrics. @@ -94,13 +97,16 @@ class Metrics(): # Convert from Decicelsius to Kelvin. heat_target = int(message.heat_target) / 10 - 273.2 + # Convert filter_life from hours to seconds + filter_life = int(message.filter_life) * 60 * 60 + self.oscillation.labels(name=name, serial=serial).state(message.oscillation) self.focus_mode.labels(name=name, serial=serial).state(message.focus_mode) self.heat_mode.labels(name=name, serial=serial).state(message.heat_mode) self.heat_state.labels(name=name, serial=serial).state(message.heat_state) self.heat_target.labels(name=name, serial=serial).set(heat_target) self.quality_target.labels(name=name, serial=serial).set(message.quality_target) - self.filter_life.labels(name=name, serial=serial).set(message.filter_life) + self.filter_life.labels(name=name, serial=serial).set(filter_life) else: logging.warning('Received unknown update from "%s" (serial=%s): %s; ignoring', name, serial, type(message))