ewilken / hap-rs

Rust implementation of the Apple HomeKit Accessory Protocol (HAP)
Apache License 2.0
196 stars 33 forks source link

IOS Home App not showing accessory details #75

Open soundprojects opened 2 years ago

soundprojects commented 2 years ago

When adding accessories to the Ios Home app, pressing the accessory with force or touch or clicking accessory details does not make anything happen. It is like the app is experiencing some error. It only happens with the services I add with hap-rs

For example here is my custom irrigation system code:

use hap::{
    accessory::{HapAccessory, AccessoryInformation},
    service::{
        accessory_information::AccessoryInformationService,
        humidity_sensor::HumiditySensorService,
        temperature_sensor::TemperatureSensorService,
        HapService, valve::ValveService,
    },
    HapType, characteristic::HapCharacteristic,
};

use serde::{
    ser::{SerializeStruct, Serializer},
    Serialize,
};
use serde_json::{Value, Number};

/// Multi Sensor accessory.
#[derive(Debug, Default)]
pub struct HydroponicAccessory {
    /// ID of the Multi Sensor accessory.
    id: u64,

    /// Accessory Information service.
    pub accessory_information: AccessoryInformationService,
    /// Temperature Sensor service.
    pub temperature_sensor: TemperatureSensorService,
    /// TDS Sensor service.
    pub tds_sensor: HumiditySensorService,
    /// Valve service
    pub valve: ValveService
}

impl HydroponicAccessory{
    pub fn new(id: u64, acc_info: AccessoryInformation) -> Self{

        let mut hydroponic = HydroponicAccessory {
            id,
            accessory_information: acc_info.to_service(1,1).expect("Failed to create service"),
            // accessory information service ends at IID 6, so we start counting at 7
            temperature_sensor: TemperatureSensorService::new(7, 1),
            // teperature sensor service ends at IID 13, so we start counting at 14
            tds_sensor: HumiditySensorService::new(14, 1),
            // humidity sensor service ends at IID 20, so we start counting at 21
            valve: ValveService::new(21, 1)
        };

        hydroponic.valve.set_primary(true);
        hydroponic.valve.active.set_value(Value::Number(Number::from(1)));

        if let Some(ref mut act)  = hydroponic.temperature_sensor.status_active{
            act.set_value(Value::Number(Number::from(1)));
        }

        if let Some(ref mut act)  = hydroponic.tds_sensor.status_active{
            act.set_value(Value::Number(Number::from(1)));
        }

        hydroponic
    }
}

impl HapAccessory for HydroponicAccessory {

    fn get_id(&self) -> u64 { self.id }

    fn set_id(&mut self, id: u64) { self.id = id; }

    fn get_service(&self, hap_type: HapType) -> Option<&dyn HapService> {
        for service in self.get_services() {
            if service.get_type() == hap_type {
                return Some(service);
            }
        }
        None
    }

    fn get_mut_service(&mut self, hap_type: HapType) -> Option<&mut dyn HapService> {
        for service in self.get_mut_services() {
            if service.get_type() == hap_type {
                return Some(service);
            }
        }
        None
    }

    fn get_services(&self) -> Vec<&dyn HapService> {
        vec![
            &self.accessory_information,
            &self.temperature_sensor,
            &self.tds_sensor,
            &self.valve
        ]
    }

    fn get_mut_services(&mut self) -> Vec<&mut dyn HapService> {
        vec![
            &mut self.accessory_information,
            &mut self.temperature_sensor,
            &mut self.tds_sensor,
            &mut self.valve
        ]
    }
}

impl Serialize for HydroponicAccessory {
    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
        let mut state = serializer.serialize_struct("HapAccessory", 2)?;
        state.serialize_field("aid", &self.get_id())?;
        state.serialize_field("services", &self.get_services())?;
        state.end()
    }
}

It connects properly but the valve is absent from the configuration and pressing the accessory to show its details does not do anything at all.

What would be a good starting point to look into this? The dnssd seems to work properly. Debug shows that TCP streams are setup. How can you verify that what the service needs to send as configuration is what the ios app is expecting?

Thanks

soundprojects commented 2 years ago

So with some help of the Homekit Accessory Simulator, I figured out what was missing

This might be useful to put in the documentation:

In the example above, the IOS home app expects the 'status_active' of the temperature and humidity sensor to be set to true and most importantly, it expects the 'is_configured' value of the valve to be set to true as well

The Active and In Use values characteristics then indicate whether the Valve is Active and (if either manually or some automatic program you develop is running) you set the In Use to true. These two values set to true make the switch visible in the 'on' state and the labels set to 'on'

When this is set using the following code:

    if let Some(ref mut conf) = hydro.valve.is_configured{
        conf.set_value(Value::Number(Number::from(1))).await.unwrap();
    }

    if let Some(ref mut conf) = hydro.temperature_sensor.status_active{
        conf.set_value(Value::Number(Number::from(1))).await.unwrap();
    }

    if let Some(ref mut conf) = hydro.tds_sensor.status_active{
        conf.set_value(Value::Number(Number::from(1))).await.unwrap();
    }

The application works as expected. There might be a prettier way of setting the values instead of the above and I think it would be nice if you could just have methods on the services and characteristics that make more sense instead of the generic 'set_value'. Maybe we could create some macro to generate methods like 'set_active' / 'set_inactive' that could be used within an async update_method to update a service without the need for a running timed loop...?

ewilken commented 2 years ago

Good find!

There might be a prettier way of setting the values instead of the above and I think it would be nice if you could just have methods on the services and characteristics that make more sense instead of the generic 'set_value'. Maybe we could create some macro to generate methods like 'set_active' / 'set_inactive' that could be used within an async update_method to update a service without the need for a running timed loop...?

And good point! Feel free to experiment on that. I'm happy to accept API improvements that make sense to you!