I'm also trying to parse things from the Maker API in a Rust app. I agree the data format returned doesn't make much sense. I can't see any good reason for representing data associated with other data implicitly by array position in JSON instead of just using an object... that's the whole point of objects.
I was also bitten by the way the devices/all endpoint returns completely different data than the individual devices/id endpoint. AFAIK none of this stuff is documented. It's a poorly designed API but unfortunately any improvements to the maker API seem to be off the table. It's up to us if we want to write a better API app.
Anyway, here's how I solved it with serde in Rust @zbdrv, it doesn't handle all cases of device attributes yet but it can effectively deserialize responses from the /devices endpoint. No custom deserializer needed.
usage snippet:
let req_url = format!(
"http://{}/apps/api/{}/devices/*?access_token={}",
self.config.host, self.config.maker_app_id, self.config.access_token
);
info!("Hubitat refresh devices {}", &req_url);
let req = reqwest::get(&req_url)
.await
.map_err(|e| error!(?e, "error making refresh devices req"))
.unwrap();
let response = req.text().await.unwrap();
debug!(?response, "got hub response");
let hub_state: Vec<hubitat_types::HubitatDevice> =
serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&response))
.unwrap();
Data types for deserialization:
use serde;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::HashMap;
/// Type for deserializing a response from the Hubitat maker API /devices endpoint
/// Note that this will _not_ work with the output of /devices/all, that endpoint returns invalid data
/// Use /devices/* or devices/:id instead to get valid data
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HubitatDevice {
pub id: String,
pub name: String,
pub label: String,
#[serde(rename = "type")]
pub type_field: String,
pub attributes: Vec<DeviceAttribute>,
pub capabilities: Vec<DeviceCapability>,
pub commands: Vec<String>,
//capture any unknown/extra fields that get returned in a HashMap
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
/// Use an untagged enum to deserialize the bad capabilities data from Hubitat maker api
/// This will properly deserialize the mixed data types in the array
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DeviceCapability {
Capability(Capability),
CapabilityEntry(CapabilityEntry),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityEntry {
pub attributes: Vec<CapabilityEntryAttribute>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityEntryAttribute {
pub name: String,
pub data_type: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StringAttribute {
pub name: String,
pub current_value: Option<String>,
// pub data_type: String,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnumAttribute {
pub name: String,
pub current_value: String,
// pub data_type: DataTypeEnum,
pub values: Vec<String>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NumberAttribute {
pub name: String,
pub current_value: f64,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnknownAttribute {
pub name: String,
pub current_value: Value,
pub data_type: String,
pub values: Option<Vec<Value>>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
/// tagged enum that keys on the dataType field to mat STRING, ENUM, NUMBER
/// todo: add other types if documentation can be found
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "dataType")]
pub enum DeviceAttribute {
#[serde(rename = "STRING")]
String(StringAttribute),
#[serde(rename = "ENUM")]
Enum(EnumAttribute),
#[serde(rename = "NUMBER")]
Number(NumberAttribute),
#[serde(other)]
Unknown,
}
/// Hubitat capability classes
/// from https://docs.hubitat.com/index.php?title=Driver_Capability_List
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Capability {
AccelerationSensor,
Actuator,
AirQuality,
Alarm,
AudioNotification,
AudioVolume,
Battery,
Beacon,
Bulb,
Button,
CarbonDioxideMeasurement,
CarbonMonoxideDetector,
ChangeLevel,
Chime,
ColorControl,
ColorMode,
ColorTemperature,
Configuration,
Consumable,
ContactSensor,
CurrentMeter,
DoorControl,
DoubleTapableButton,
EnergyMeter,
EstimatedTimeOfArrival,
FanControl,
FilterStatus,
Flash,
GarageDoorControl,
GasDetector,
HealthCheck,
HoldableButton,
IlluminanceMeasurement,
ImageCapture,
Indicator,
Initialize,
LevelPreset,
Light,
LightEffects,
LiquidFlowRate,
LocationMode,
Lock,
LockCodes,
MediaController,
MediaInputSource,
MediaTransport,
Momentary,
MotionSensor,
MusicPlayer,
Notification,
Outlet,
Polling,
PowerMeter,
PowerSource,
PresenceSensor,
PressureMeasurement,
PushableButton,
Refresh,
RelativeHumidityMeasurement,
RelaySwitch,
ReleasableButton,
SamsungTV,
SecurityKeypad,
Sensor,
ShockSensor,
SignalStrength,
SleepSensor,
SmokeDetector,
SoundPressureLevel,
SoundSensor,
SpeechRecognition,
SpeechSynthesis,
StepSensor,
Switch,
SwitchLevel,
TV,
TamperAlert,
Telnet,
TemperatureMeasurement,
TestCapability,
Thermostat,
ThermostatCoolingSetpoint,
ThermostatFanMode,
ThermostatHeatingSetpoint,
ThermostatMode,
ThermostatOperatingState,
ThermostatSchedule,
ThermostatSetpoint,
ThreeAxis,
TimedSession,
Tone,
TouchSensor,
UltravioletIndex,
Valve,
Variable,
VideoCamera,
VideoCapture,
VoltageMeasurement,
WaterSensor,
WindowBlind,
WindowShade,
ZwMultichannel,
#[serde(rename = "pHMeasurement")]
PHMeasurement,
}