Maker API: Device Attributes

Here is how I'd deal with the capabilities list, if what I wanted was a map indexed by capabilities:

Map capabs(List capabList) {
	Map result = [:]
	Integer ndx = 0
	while(ndx < capabList.size()) {
		if(ndx + 1 < capabList.size() && capabList[ndx + 1] instanceof Map) {
			result[capabList[ndx]] = capabList[ndx + 1]
			ndx++
		} else result[capabList[ndx]] = [:]
		ndx++		
	}
	result
}

This does not take two passes, and each capability that has no attributes has a null map for the missing attributes. An obvious variation would be to make a list of maps for each capability.

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,
}

You're missing a bit of perspective about this. Maker API grew a bit after its first release, which is why there are differences in the formats returned -- live and learn. Backwards compatibility greatly constrains changes to those formats, as there is software out there relying on the specifics of what is returned.

Improvements are by no means off the table, but need to take the above into account. Specific requests are usually dealt with in due course. Criticism for its own sake is pointless.

2 Likes

Sorry, I didn't intend to come off as overly critical. I read your comments above as dismissing the possibility of improvements due to the need for backwards compatibility, which I agree is important!

Here are some specific suggestions for how I think the maker API might be improved given my usage of it so far:

  • More documentation in general. The current docs are very sparse. In specific:
    • Document the /devices/* endpoint. Before I stumbled across it here I thought I would need to use devices/all to fetch ids then call /devices/id for each id to get data on all available devices. The current docs recommend the /devices/all endpoint.
    • Document each endpoint and the format of the JSON it returns.
    • Document cases where endpoints return specific formats of data for backwards compatibility such as the /devices/all endpoint.
    • Document which commands can be called via the API. Currently the docs say NOTE: There is a limited subset of allowed commands, so just because a command shows up in this list, does not mean it will work via the API. and the app page says a supported command but I haven't found any documentation of which commands are supported. The app page also says ...have the ability to send any command to the device which is somewhat confusing since the same page and docs indicate that only a subset of commands will work.
    • Document performance and rate limit constraints. I've discovered that the API will return errors if you send too many requests but this behavior or the actual limitations aren't documented AFAIK.
    • Document error/failure responses
  • A batch command endpoint could be useful. Since the API doesn't accept a lot of simultaneous requests it would be nice to send a single request with a batch of commands to be executed.
  • Versioned endpoints. For example /apps/api/xxx/v1/devices/all which return the format it does now and /apps/api/xxx/v2/devices/all which returns the same as /devices/*. That would allow for improvements without breaking backwards compatibility.

This is on the list of projects for our documentation team, prioritized appropriately (not super high, given its esoteric nature).

As long a any new endpoint is to be defined, it might as well be new, without constraints of existing endpoint definitions. Open to suggestions as to what should be done with this. Not keen on perpetuating a "poorly designed API".

Can't very well change any existing endpoint to add versioning. Beyond that, it's a new endpoint and needs a new definition. If that definition includes versioning, OK.

Perhaps you or another Maker API enthusiast should organize a Maker API user group, and come up with a proposal for new endpoints...

4 Likes

This is coming from someone who is programming illiterate, so take this with a hefty pinch of salt.

  1. Leave the current Maker-API alone
  2. With, or without, community participation, create the parameters that will be served by v2 of Maker-API. New integrations will use the newer version while old integrations will still work.
  3. If needed, Set a deadline for when the old version will no longer work.

This is the pinch of salt part. Can't do. How would you like to wake up some day and your automated house was dead, all because we pulled the plug on something you counted on? Leaving old versions of things running is a requirement. Backwards compatibility of changes is a requirement. These are not hindrances to adding new capabilities that are different than what is already there.

There is no compelling reason to create a v2 Maker API. While some users are annoyed by its design, or warts thereof, it does work and can be made to do what is needed. It's not quite a pig's ear, but there's no reason for a silk purse for this. 'Oh, I had to write some code to get it to work the way I want.' By definition a Maker API user is capable of doing this as needed, and it is expected. This will most likely be true no matter what the design actually is. The point is, one can write some code and use Maker API to get the raw elements needed -- not you @aaiyar, but those who need it.

5 Likes

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.