From 851e349dac4728c14dedb3dbdc3db4eb979702ce Mon Sep 17 00:00:00 2001 From: Aaron Holmes Date: Sat, 21 Oct 2023 15:56:44 -0700 Subject: [PATCH] Add a RachioController device type. --- .../HubitatDevice/DeviceTypes/DeviceType.cs | 3 + .../HubitatDevice/DeviceTypes/FlumeDevice.cs | 61 +++++ .../DeviceTypes/GenericDevice.cs | 29 ++- .../Rachio/RachioControllerDevice.cs | 238 ++++++++++++++++++ 4 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 hubitat2prom/HubitatDevice/DeviceTypes/Rachio/RachioControllerDevice.cs diff --git a/hubitat2prom/HubitatDevice/DeviceTypes/DeviceType.cs b/hubitat2prom/HubitatDevice/DeviceTypes/DeviceType.cs index b911bf3..da9bdeb 100644 --- a/hubitat2prom/HubitatDevice/DeviceTypes/DeviceType.cs +++ b/hubitat2prom/HubitatDevice/DeviceTypes/DeviceType.cs @@ -1,5 +1,6 @@ using AttributeValue = OneOf.OneOf?>; using hubitat2prom.HubitatDevice; +using Rachio = hubitat2prom.HubitatDevice.DeviceTypes.Rachio; namespace hubitat2prom.PrometheusExporter.DeviceTypes; @@ -16,6 +17,8 @@ public static DeviceType CreateDeviceType(DeviceSummary deviceSummary) { case "Flume Device": return new FlumeDevice(); + case "Rachio Controller": + return new Rachio.RachioControllerDevice(); default: return new GenericDevice(); } diff --git a/hubitat2prom/HubitatDevice/DeviceTypes/FlumeDevice.cs b/hubitat2prom/HubitatDevice/DeviceTypes/FlumeDevice.cs index af1fd45..703c99e 100644 --- a/hubitat2prom/HubitatDevice/DeviceTypes/FlumeDevice.cs +++ b/hubitat2prom/HubitatDevice/DeviceTypes/FlumeDevice.cs @@ -3,6 +3,67 @@ namespace hubitat2prom.PrometheusExporter.DeviceTypes; +/// +/// Flume Device. The JSON response is structured like: +/// { +/// "name": "Flume", +/// "label": "Flume", +/// "type": "Flume Device", +/// "id": "1319", +/// "date": "2023-10-21T20:09:39+0000", +/// "model": null, +/// "manufacturer": null, +/// "room": "Outside", +/// "capabilities": [ +/// "WaterSensor", +/// "Configuration", +/// "Refresh", +/// "Initialize", +/// "PresenceSensor" +/// ], +/// "attributes": { +/// "usageLastDay": "111.02", +/// "dataType": "ENUM", +/// "values": [ +/// "present", +/// "not present" +/// ], +/// "usageLastHour": "0.0", +/// "notificationStream": "[2023-09-30T01:55:00.000Z]: High Flow Alert triggered at 4243 Coolidge. Water has been running for 15 minutes averaging 9.06 gallons every minute.", +/// "water": "dry", +/// "usageLastMonth": "2594.13", +/// "usageLastWeek": "548.59", +/// "flowStatus": "stopped", +/// "commStatus": "good", +/// "flowDurationMin": "0", +/// "usageLastMinute": "0.0", +/// "presence": "present" +/// }, +/// "commands": [ +/// { +/// "command": "clearAwayMode" +/// }, +/// { +/// "command": "clearWetStatus" +/// }, +/// { +/// "command": "configure" +/// }, +/// { +/// "command": "initialize" +/// }, +/// { +/// "command": "refresh" +/// }, +/// { +/// "command": "setAwayMode" +/// }, +/// { +/// "command": "testWetStatus" +/// } +/// ] +/// }, +/// public class FlumeDevice: GenericDevice { public FlumeDevice() { } diff --git a/hubitat2prom/HubitatDevice/DeviceTypes/GenericDevice.cs b/hubitat2prom/HubitatDevice/DeviceTypes/GenericDevice.cs index f8ffb00..7201059 100644 --- a/hubitat2prom/HubitatDevice/DeviceTypes/GenericDevice.cs +++ b/hubitat2prom/HubitatDevice/DeviceTypes/GenericDevice.cs @@ -13,12 +13,19 @@ public override double ExtractMetric(string attributeName, AttributeValue attrib @string => { var name = attributeName.ToLowerInvariant(); + // TODO consider removing name=="switch" and + // instead put TryGetBoolean first. if (name == "switch") return @string == "on" ? 1 : 0; if (name == "power" && TryGetPower(@string, out value)) return value; if (name == "thermostatoperatingstate" && TryGetThermostatOperatingState(@string, out value)) return value; if (name == "thermostatmode" && TryGetThermostatMode(@string, out value)) return value; if (name == "contact") return @string == "closed" ? 1 : 0; - + + // many devices use on/off/true/false for different + // attributes. try to capture that here before + // bailing out with the default value. + if (TryGetBoolean(@string, out value)) return value; + return MISSING_VALUE_DEFAULT; }, stringArray => MISSING_VALUE_DEFAULT, @@ -35,6 +42,26 @@ public override double ExtractMetric(string attributeName, AttributeValue attrib return metricValue; } + private static bool TryGetBoolean(string stringValue, out double value) + { + stringValue = stringValue.ToLowerInvariant(); + + if (stringValue == "off" || stringValue == "false") + { + value = 0; + return true; + } + + if (stringValue == "on" || stringValue == "true") + { + value = 1; + return true; + } + + value = -1; + return false; + } + private static bool TryGetPower(string power, out double value) { if (power == "off") diff --git a/hubitat2prom/HubitatDevice/DeviceTypes/Rachio/RachioControllerDevice.cs b/hubitat2prom/HubitatDevice/DeviceTypes/Rachio/RachioControllerDevice.cs new file mode 100644 index 0000000..e334139 --- /dev/null +++ b/hubitat2prom/HubitatDevice/DeviceTypes/Rachio/RachioControllerDevice.cs @@ -0,0 +1,238 @@ +using hubitat2prom.PrometheusExporter.DeviceTypes; +using AttributeValue = OneOf.OneOf?>; + +namespace hubitat2prom.HubitatDevice.DeviceTypes.Rachio; + + +/// +/// Rachio Controller Device. The JSON response is structured like: +/// { +/// "name": "Rachio Controller", +/// "label": "Rachio - Rachio", +/// "type": "Rachio Controller", +/// "id": "1389", +/// "date": "2023-10-21T20:20:16+0000", +/// "model": null, +/// "manufacturer": null, +/// "room": null, +/// "capabilities": [ +/// "Actuator", +/// "Refresh", +/// "Polling", +/// "WaterSensor", +/// "Valve", +/// "Switch", +/// "Sensor", +/// "HealthCheck" +/// ], +/// "attributes": { +/// "rainSensorTripped": "false", +/// "dataType": "STRING", +/// "values": null, +/// "activeZoneCnt": "3", +/// "checkInterval": null, +/// "nextEventTime": "Oct 26, 2023 - 6:40:07 AM", +/// "controllerOn": "true", +/// "curZoneName": "...", +/// "curZoneRunStatus": "Status: Idle", +/// "notificationMessage": null, +/// "nextEvent": "..." +/// "water": "dry", +/// "hardwareDesc": "8-Zone (Gen 3)", +/// "nextRunStr": "Oct 26, 2023 - 6:40:07 AM", +/// "nextEventWeatherIntelligence": null, +/// "lastRun": "1697488267000", +/// "valve": "close", +/// "watering": "off", +/// "curZoneNumber": "3", +/// "lastUpdatedDt": "Oct 21, 2023 - 1:20:16 PM", +/// "rainDelay": "0", +/// "nextRun": "1698352807000", +/// "curZoneStartDate": "Not Active", +/// "rainDelayStr": "No Rain Delay", +/// "curZoneDuration": "1673", +/// "monthlyWeatherIntelligenceCount": "0", +/// "curZoneIsCycling": "False", +/// "DeviceWatch-DeviceStatus": "online", +/// "scheduleType": "Off", +/// "switch": "off", +/// "monthlyMinutesSaved": "0.0", +/// "standbyMode": "off", +/// "curZoneCycleCount": "0", +/// "nextEventThresholdInfo": null, +/// "nextEventType": "FUTURE_FEED_SCHEDULE_EVENT", +/// "monthlyMinutesUsed": "56.0", +/// "dashboard": "...", +/// "curZoneWaterTime": "10", +/// "hardwareModel": "8ZoneV3", +/// "lastRunStr": "Oct 16, 2023 - 6:31:07 AM" +/// }, +/// "commands": [ +/// { +/// "command": "close" +/// }, +/// { +/// "command": "decZoneWaterTime" +/// }, +/// { +/// "command": "decreaseRainDelay" +/// }, +/// { +/// "command": "doSetRainDelay" +/// }, +/// { +/// "command": "incZoneWaterTime" +/// }, +/// { +/// "command": "increaseRainDelay" +/// }, +/// { +/// "command": "off" +/// }, +/// { +/// "command": "on" +/// }, +/// { +/// "command": "open" +/// }, +/// { +/// "command": "pauseZoneRun" +/// }, +/// { +/// "command": "ping" +/// }, +/// { +/// "command": "poll" +/// }, +/// { +/// "command": "refresh" +/// }, +/// { +/// "command": "resumeZoneRun" +/// }, +/// { +/// "command": "runAllZones" +/// }, +/// { +/// "command": "setDashboardColorScheme" +/// }, +/// { +/// "command": "setDashboardIconScheme" +/// }, +/// { +/// "command": "setRainDelay" +/// }, +/// { +/// "command": "setZoneWaterTime" +/// }, +/// { +/// "command": "standbyOff" +/// }, +/// { +/// "command": "standbyOn" +/// }, +/// { +/// "command": "stopWatering" +/// } +/// ] +/// }, +/// +public class RachioControllerDevice : GenericDevice +{ + public RachioControllerDevice() { } + + public override double ExtractMetric(string attributeName, AttributeValue attributeValue) + { + // We can return if the value type is not T0 (string) + // because that is the only type this method extracts + if (!attributeValue.IsT0) return base.ExtractMetric(attributeName, attributeValue); + var attributeStringValue = attributeValue.AsT0; + double value; + // TODO Rachio has a lot of date-type fields + // figure out their formats and add them as timestamps + switch (attributeName) + { + case "curzonerunstatus": + if (TryGetCurrentZoneRunStatus(attributeStringValue, out value)) return value; + break; + case "water": + if (TryGetWater(attributeStringValue, out value)) return value; + break; + case "valve": + if (TryGetValve(attributeStringValue, out value)) return value; + break; + case "devicewatch-devicestatus": + if (TryGetDeviceWatchDeviceStatus(attributeStringValue, out value)) return value; + break; + case "nexteventtype": + if (TryGetNextEventType(attributeStringValue, out value)) return value; + break; + default: + System.Diagnostics.Debug.WriteLine($"Unknown attribute \"{attributeName}\" from Flume device."); + break; + } + + return MISSING_VALUE_DEFAULT; + } + + private static bool TryGetCurrentZoneRunStatus(string zoneRunStatus, out double value) + { + switch (zoneRunStatus) + { + case "Status: Idle": value = 0; return true; + // TODO are these correct? + case "Device is Offline": value = 1; return true; + case "Device in Standby Mode": value = 2; return true; + } + + value = -1; + return false; + } + + private static bool TryGetValve(string valve, out double value) + { + switch (valve) + { + case "close": value = 0; return true; + case "open": value = 1; return true; + } + + value = -1; + return false; + } + + private static bool TryGetDeviceWatchDeviceStatus(string deviceWatchDeviceStatus, out double value) + { + switch (deviceWatchDeviceStatus) + { + case "offline": value = 0; return true; + case "online": value = 1; return true; + } + + value = -1; + return false; + } + + private static bool TryGetWater(string water, out double value) + { + switch (water) + { + case "dry": value = 0; return true; + case "wet": value = 1; return true; + } + + value = -1; + return false; + } + + private static bool TryGetNextEventType(string water, out double value) + { + switch (water) + { + case "FUTURE_FEED_SCHEDULE_EVENT": value = 0; return true; + } + + value = -1; + return false; + } +} \ No newline at end of file