diff --git a/examples/Expert/13-EventNotifications/13-EventNotifications.ino b/examples/Expert/13-EventNotifications/13-EventNotifications.ino index 3ca8be2..8e38197 100644 --- a/examples/Expert/13-EventNotifications/13-EventNotifications.ino +++ b/examples/Expert/13-EventNotifications/13-EventNotifications.ino @@ -5,22 +5,64 @@ // ------------------------------------------------ // // // // Example 13: Event Notifications // -// - implementing a temperature sensor // +// * implementing a temperature sensor // +// * implementing an air quality sensor // // // //////////////////////////////////////////////////////////// #include "HomeSpan.h" -#include "DEV_LED.h" #include "DEV_Identify.h" #include "DEV_Sensors.h" void setup() { - // Example 11 illustrates how to control an RGB LED to set any color and brightness. - // The configuration below should look familiar by now. We've created a new derived Service, - // call RgbLED to house all the required logic. You'll find all the code in DEV_LED.h. - // For completeness, this configuration also contains an on/off LED and a dimmable LED as shown - // in prior examples. + // HomeKit is designed for two-way communication: HomeSpan devices not only receive and act on operational instructions from HomeKit Controllers, but + // HomeSpan can also send HomeKit unsolicited messages regarding changes to the state of the device. Though it may not be apparent, this has already been + // ocurring in the background in all prior examples. This is because when a HomeKit Controller sends an operational request to any HomeKit device, it expects + // to receive a status message back indicating whether the request was successful or not. This is the purpose of returning StatusCode:OK in custom update() + // methods. With this information returned, HomeKit can update its own status and properly reflect a change in the device, such as by showing a light is now + // turned on instead of off. However, HomeKit unfortunately does NOT inform any other HomeKit Controllers of this new information. So if you have two iPhones + // and use one to turn on a light, the other first iPhone does not relay a message to the second iPhone that a light has been turned on. This is the case even + // if you are using an AppleTV or HomePod as a central hub for HomeKit. + + // Normally this does not matter much, since the second iPhone will naturally update itself as to the status of all HomeKit devices as soon as the HomeKit + // application is launched on that iPhone. It does this by sending every HomeKit device a message asking for a status update. In this fashion the second + // iPhone quickly synchronizes itself as soon as the HomeKit app is opened, but ONLY when it is first opened (or re-opened if you first close it). But if you + // have two iPhones BOTH opened to the HomeKit app (or one iPhone and one Mac opened to the HomeKit app) and you use one Controller app to turn on a light, the + // resulting change in status of that light will NOT be reflected in the second Controller app, unless you close tha app and re-open (at which point it goes + // through the request procedure discussed above). This is very annoying and counterintuitive. + + // Fortunately, HomeKit provides a solution to this in the form of an Event Notification protcol. This protcol allows a device to send unsoliciated messages + // to all Controllers that have previously registered themselves with the device indicating the Characteristics for which they would like to receive an event + // message from the device whenever there is a change in the status of one or more of those Characteristics. + + // The good news is that HomeSpan takes care of this automatically. To see this for yourself, use two iPhones (or an iPhone and Mac) with any of the previous examples + // and open the HomeKit app on both. Any changes you make to the device using one of the Controllers, such as turning on an LED, is immediately reflected + // in the other Controller. Not quite magic, but close. + + // A different use of Event Notifications was also working behind in the scenes in Example 10 - Timed Resets. In this case, HomeSpan sent an unsolited Event message + // to all registered Controllers letting them know that a device that was previously turned on, is now in fact turned off. + + // In this Example 13 we explore the explicit use of Event Notifications to support Services that require constants updates from the device to all HomeKit Controllers. + // The two Services we will use below are a Temperature Sensor and an Air Quality Sensor. Neither of these Services have any operational controls. They cannot be + // turn on or off, or operated in any way. As such, they do not need to implement an update() method, since HomeKit Controllers will never ask them to change + // any of their Characteristics. + + // Rather, HomeKit is expecting to get periodic Event Notification messages from such Services so that the HomeKit Controllers can accurately reflect the status + // and values of the Characteristics for those Services, such as the temperature, in the HomeKit Controller. + + // There are two steps to accomplishing this. The first is to implement an event() method for each Service that uses a setVal() function to change the values + // for one or more Characteristics for that Service. The second step is to instantiate a new SpanEvent() object for each Service that you want HomeSpan to invoke your + // event() method. The SpanEvent object take only one argument - the number of milliseconds to wait between calls to a Service's event() method. + + // As usual, all of the logic for this is encapsulated in new standalone derived Services. You'll find fully-commented definitions for the DEV_TempSensor() and + // the DEV_AirQualitySensor() Services instantiated below, in the DEV_Sensors.h file. Note that this example is for instructional purposes only -- we do not actually + // connect a Temperature Sensor or Air Quality Sensor to our ESP32 device. As such, we did not define the Services to take any arguments to specify pin numbers or any + // other information needed to implement an actual sensor. Instead, in order to see how real a device would work, we will send Event messages by manufacturing simulated + // updates. See DEV_Sensors.h for complete details. + + // Once you understand these examples, you should be able to use Event Notifications for any combination of HomeKit Services with Characteristics that require your device to + // send periodic update messages to HomeKit Controllers, ranging from Smoke Alarms to Door Sensors. Serial.begin(115200); @@ -31,27 +73,14 @@ void setup() { new DEV_Identify("Bridge #1","HomeSpan","123-ABC","HS Bridge","0.9",3); new Service::HAPProtocolInformation(); new Characteristic::Version("1.1.0"); - - new SpanAccessory(); - new DEV_Identify("On/Off LED","HomeSpan","123-ABC","20mA LED","0.9",0); - new DEV_LED(16); // Create an On/Off LED attached to pin 16 - - new SpanAccessory(); - new DEV_Identify("Dimmable LED","HomeSpan","123-ABC","20mA LED","0.9",0); - new DEV_DimmableLED(0,17); // Create a Dimmable LED using PWM channel 0, attached to pin 17 - - new SpanAccessory(); - new DEV_Identify("RGB LED","HomeSpan","123-ABC","20mA LED","0.9",0); - new DEV_RgbLED(1,2,3,32,22,23); // Create an RGB LED using PWM channels 1,2,3, attached to pins 32,22,23 (for R, G, and B LED anodes) new SpanAccessory(); - new DEV_Identify("Temp Sensor","HomeSpan","123-ABC","Celsius","0.9",0); - new DEV_TempSensor(); // Create a Temperature Sensor + new DEV_Identify("Temp Sensor","HomeSpan","123-ABC","Sensor","0.9",0); + new DEV_TempSensor(); // Create a Temperature Sensor (see DEV_Sensors.h for definition) new SpanAccessory(); new DEV_Identify("Air Quality","HomeSpan","123-ABC","Sensor","0.9",0); - new DEV_AirQualitySensor(); // Create an Air Quality Sensor - new SpanEvent(10000); + new DEV_AirQualitySensor(); // Create an Air Quality Sensor (see DEV_Sensors.h for definition) } // end of setup() diff --git a/examples/Expert/13-EventNotifications/DEV_Identify.h b/examples/Expert/13-EventNotifications/DEV_Identify.h index 7e6826f..95ba92a 100644 --- a/examples/Expert/13-EventNotifications/DEV_Identify.h +++ b/examples/Expert/13-EventNotifications/DEV_Identify.h @@ -7,8 +7,10 @@ struct DEV_Identify : Service::AccessoryInformation { int nBlinks; // number of times to blink built-in LED in identify routine SpanCharacteristic *identify; // reference to the Identify Characteristic + + // NEW! modified constructor() method to include optional ServiceType argument - DEV_Identify(char *name, char *manu, char *sn, char *model, char *version, int nBlinks) : Service::AccessoryInformation(){ + DEV_Identify(char *name, char *manu, char *sn, char *model, char *version, int nBlinks, ServiceType sType=ServiceType::Regular) : Service::AccessoryInformation(sType){ new Characteristic::Name(name); // create all the required Characteristics with values set based on above arguments new Characteristic::Manufacturer(manu); diff --git a/examples/Expert/13-EventNotifications/DEV_LED.h b/examples/Expert/13-EventNotifications/DEV_LED.h deleted file mode 100644 index 49c4662..0000000 --- a/examples/Expert/13-EventNotifications/DEV_LED.h +++ /dev/null @@ -1,205 +0,0 @@ - -//////////////////////////////////// -// DEVICE-SPECIFIC LED SERVICES // -//////////////////////////////////// - -#include "extras/PwmPin.h" // library of various PWM functions - -//////////////////////////////////// - -struct DEV_LED : Service::LightBulb { // ON/OFF LED - - int ledPin; // pin number defined for this LED - SpanCharacteristic *power; // reference to the On Characteristic - - DEV_LED(int ledPin) : Service::LightBulb(){ // constructor() method - - power=new Characteristic::On(); - this->ledPin=ledPin; - pinMode(ledPin,OUTPUT); - - Serial.print("Configuring On/Off LED: Pin="); // initialization message - Serial.print(ledPin); - Serial.print("\n"); - - } // end constructor - - StatusCode update(){ // update() method - - LOG1("Updating On/Off LED on pin="); - LOG1(ledPin); - LOG1(": Current Power="); - LOG1(power->getVal()?"true":"false"); - LOG1(" New Power="); - LOG1(power->getNewVal()?"true":"false"); - LOG1("\n"); - - digitalWrite(ledPin,power->getNewVal()); - - return(StatusCode::OK); // return OK status code - - } // update -}; - -////////////////////////////////// - -struct DEV_DimmableLED : Service::LightBulb { // Dimmable LED - - PwmPin *pwmPin; // reference to PWM Pin - int ledPin; // pin number defined for this LED - int channel; // PWM channel used for this LED (should be unique for each LED) - SpanCharacteristic *power; // reference to the On Characteristic - SpanCharacteristic *level; // reference to the Brightness Characteristic - - DEV_DimmableLED(int channel, int ledPin) : Service::LightBulb(){ // constructor() method - - power=new Characteristic::On(); - - level=new Characteristic::Brightness(50); // Brightness Characteristic with an initial value of 50% - new SpanRange(5,100,1); // sets the range of the Brightness to be from a min of 5%, to a max of 100%, in steps of 1% - - this->channel=channel; // save the channel number (from 0-15) - this->ledPin=ledPin; // save LED pin number - this->pwmPin=new PwmPin(channel, ledPin); // configure the PWM channel and attach the specified ledPin - - Serial.print("Configuring Dimmable LED: Pin="); // initialization message - Serial.print(ledPin); - Serial.print(" Channel="); - Serial.print(channel); - Serial.print("\n"); - - } // end constructor - - StatusCode update(){ // update() method - - LOG1("Updating Dimmable LED on pin="); - LOG1(ledPin); - LOG1(": Current Power="); - LOG1(power->getVal()?"true":"false"); - LOG1(" Current Brightness="); - LOG1(level->getVal()); - - if(power->updated()){ - LOG1(" New Power="); - LOG1(power->getNewVal()?"true":"false"); - } - - if(level->updated()){ - LOG1(" New Brightness="); - LOG1(level->getNewVal()); - } - - LOG1("\n"); - - pwmPin->set(channel,power->getNewVal()*level->getNewVal()); - - return(StatusCode::OK); // return OK status code - - } // update -}; - -////////////////////////////////// - -struct DEV_RgbLED : Service::LightBulb { // RGB LED (Command Cathode) - - PwmPin *redPin, *greenPin, *bluePin; - int redChannel, greenChannel, blueChannel; - - SpanCharacteristic *power; // reference to the On Characteristic - SpanCharacteristic *H; // reference to the Hue Characteristic - SpanCharacteristic *S; // reference to the Saturation Characteristic - SpanCharacteristic *V; // reference to the Brightness Characteristic - - DEV_RgbLED(int redChannel, int greenChannel, int blueChannel, int redPin, int greenPin, int bluePin) : Service::LightBulb(){ // constructor() method - - power=new Characteristic::On(); - H=new Characteristic::Hue(0); // instantiate the Hue Characteristic with an initial value of 0 out of 360 - S=new Characteristic::Saturation(0); // instantiate the Saturation Characteristic with an initial value of 0% - V=new Characteristic::Brightness(100); // instantiate the Brightness Characteristic with an initial value of 100% - new SpanRange(5,100,1); // sets the range of the Brightness to be from a min of 5%, to a max of 100%, in steps of 1% - - this->redChannel=redChannel; // save the channel number (from 0-15) - this->greenChannel=greenChannel; - this->blueChannel=blueChannel; - - this->redPin=new PwmPin(redChannel, redPin); // instantiate the PWM channel and attach the specified pin - this->greenPin=new PwmPin(greenChannel, greenPin); - this->bluePin=new PwmPin(blueChannel, bluePin); - - char cBuf[128]; - sprintf(cBuf,"Configuring RGB LED: Pins=(%d,%d,%d) Channels=(%d,%d,%d)\n",redPin,greenPin,bluePin,redChannel,greenChannel,blueChannel); - Serial.print(cBuf); - - } // end constructor - - StatusCode update(){ // update() method - - boolean p; - float v, h, s, r, g, b; - - h=H->getVal(); // get and store all current values. Note the use of the template to properly read the values - s=S->getVal(); - v=V->getVal(); // though H and S are defined as FLOAT in HAP, V (which is brightness) is defined as INT, but will be re-cast appropriately - p=power->getVal(); - - char cBuf[128]; - sprintf(cBuf,"Updating RGB LED on pins=(%d,%d,%d): ",redPin->getPin(),greenPin->getPin(),bluePin->getPin()); - LOG1(cBuf); - - if(power->updated()){ - p=power->getNewVal(); - sprintf(cBuf,"Power=%s->%s, ",power->getVal()?"true":"false",p?"true":"false"); - } else { - sprintf(cBuf,"Power=%s, ",p?"true":"false"); - } - LOG1(cBuf); - - if(H->updated()){ - h=H->getNewVal(); - sprintf(cBuf,"H=%.0f->%.0f, ",H->getVal(),h); - } else { - sprintf(cBuf,"H=%.0f, ",h); - } - LOG1(cBuf); - - if(S->updated()){ - s=S->getNewVal(); - sprintf(cBuf,"S=%.0f->%.0f, ",S->getVal(),s); - } else { - sprintf(cBuf,"S=%.0f, ",s); - } - LOG1(cBuf); - - if(V->updated()){ - v=V->getNewVal(); - sprintf(cBuf,"V=%.0f->%.0f ",V->getVal(),v); - } else { - sprintf(cBuf,"V=%.0f ",v); - } - LOG1(cBuf); - - // Here we call a static function of PwmPin that converts HSV to RGB. - // Parameters must all be floats in range of H[0,360], S[0,1], and V[0,1] - // R, G, B, returned [0,1] range as well - - PwmPin::HSVtoRGB(h,s/100.0,v/100.0,&r,&g,&b); // since HomeKit provides S and V in percent, scale down by 100 - - int R, G, B; - - R=p*r*100; // since PwmPin uses percent, scale back up by 100, and multiple by status fo power (either 0 or 1) - G=p*g*100; - B=p*b*100; - - sprintf(cBuf,"RGB=(%d,%d,%d)\n",R,G,B); - LOG1(cBuf); - - redPin->set(redChannel,R); // update the PWM channels with new values - greenPin->set(greenChannel,G); - bluePin->set(blueChannel,B); - - return(StatusCode::OK); // return OK status code - - } // update -}; - -////////////////////////////////// diff --git a/examples/Expert/13-EventNotifications/DEV_Sensors.h b/examples/Expert/13-EventNotifications/DEV_Sensors.h index 6e83e86..788d3c8 100644 --- a/examples/Expert/13-EventNotifications/DEV_Sensors.h +++ b/examples/Expert/13-EventNotifications/DEV_Sensors.h @@ -9,41 +9,70 @@ struct DEV_TempSensor : Service::TemperatureSensor { // A standalone Tempera DEV_TempSensor(ServiceType sType=ServiceType::Regular) : Service::TemperatureSensor(sType){ // constructor() method - new SpanEvent(5000); - temp=new Characteristic::CurrentTemperature(); + // We begin by defining a new SpanEvent. This instructs HomeSpan to call the Service's event() method (defined below) periodically. + // The argument to SpanEvent() defines the periodicity, in milliseconds. In this case we are instructing HomeSpan to check this Service for + // updates every 5 seconds. Checking takes time, and updates use network traffic, so choose your periodicity wisely. In practice you could + // probably set the periodicity for a temperature sensor to 60 seconds or more. But for illustrative purposes we are specifying more frequent + // updates so you can see how the this example works without needing to wait a full minute for each change. + + new SpanEvent(5000); // check for events on this Service every 5 seconds + + // Next we instantiate the main Characteristic for a Temperature Sensor, namely the Current Temperature, and set its initial value + // to 20 degrees. For a real sensor, we would take a reading and initialize it to that value instead. NOTE: HomeKit uses + // Celsius for all temperature settings. HomeKit will DISPLAY temperatures in the HomeKit app according to the settings on your iPhone. + // Though the HAP documentation includes a Characteristic that appears to allow the device to over-ride this setting by specifying a display + // of Celsius or Fahrenheit for each Service, it does not appear to work as advertised. + + temp=new Characteristic::CurrentTemperature(20.0); // instantiate the Current Temperature Characteristic Serial.print("Configuring Temperature Sensor"); // initialization message Serial.print("\n"); } // end constructor + // Lastly, we create the event() method. This method take no arguments and returns no values. It will be called every 5 seconds + // as specified above in the instantiation of SpanEvent(). In order to simulate a temperature change from an actual sensor we + // will read the current value of the temp Characteristic using the getVal() function, with as the template parameter; + // add 0.5 degrees Celsius; and then store the result in a float variable named "temperature." This will simulate an increment of + // 0.5 degrees Celsius (a little less than 1 degree F) every 5 seconds. We will cap the temperature to 35.0 degrees C, after which + // it resets to 10.0 and starts over. + + // All of the action happens in the last line, in which we set the value of the temp Characteristic to the new value of temperature. + // This tells HomeKit to send an Event Notification message to all available Controllers making them aware of the new temperature. + // Note that setVal() is NOT a template function and does not require you to specify as a template parameter. This is because + // setVal() can determine the type from the argument you specify. If there is any chance of ambiguity, you can always specifically + // cast the argument such: setVal((float)temperature). + void event(){ - float temperature=temp->getVal()+1.0; - if(temperature>35) - temperature=10; - temp->setVal(temperature); + float temperature=temp->getVal()+0.5; // here we "simulate" a half-degree temperature change... + if(temperature>35.0) // ...but cap the maximum at 35 degrees before starting over at 10 degrees + temperature=10.0; + + temp->setVal(temperature); // don't forgot to update the temperature Characteristic to the new value! } // event + }; ////////////////////////////////// struct DEV_AirQualitySensor : Service::AirQualitySensor { // A standalone Air Quality sensor - SpanCharacteristic *airQuality; - SpanCharacteristic *o3Density; - SpanCharacteristic *no2Density; - SpanCharacteristic *so2Density; - SpanCharacteristic *pm25Density; - SpanCharacteristic *pm10Density; - SpanCharacteristic *vocDensity; + // An Air Quality Sensor is similar to a Temperature Sensor except that it supports a wide variety of measurements. + // We will use three of them. The first is required, the second two are optional. + + SpanCharacteristic *airQuality; // reference to the Air Quality Characteristic, which is in integer from 0 to 5 + SpanCharacteristic *o3Density; // reference to the Ozone Density Characteristic, which is a float from 0 to 1000 + SpanCharacteristic *no2Density; // reference to the Nitrogen Dioxide Characteristic, which is a float from 0 to 1000 DEV_AirQualitySensor(ServiceType sType=ServiceType::Regular) : Service::AirQualitySensor(sType){ // constructor() method - airQuality=new Characteristic::AirQuality(1); - o3Density=new Characteristic::OzoneDensity(40.7); - no2Density=new Characteristic::NitrogenDioxideDensity(13.2); + new SpanEvent(10000); // check for events on this Service every 10 seconds + + airQuality=new Characteristic::AirQuality(1); // instantiate the Air Quality Characteristic and set initial value to 1 + o3Density=new Characteristic::OzoneDensity(300.0); // instantiate the Ozone Density Characteristic and set initial value to 300.0 + no2Density=new Characteristic::NitrogenDioxideDensity(700.0); // instantiate the Nitrogen Dioxide Density Characteristic and set initial value to 700.0 Serial.print("Configuring Air Quality Sensor"); // initialization message Serial.print("\n"); @@ -52,13 +81,16 @@ struct DEV_AirQualitySensor : Service::AirQualitySensor { // A standalone Ai void event(){ - airQuality->setVal((airQuality->getVal()+1)%6); - o3Density->setVal((double)random(200,500)); - if(!random(2)){ - no2Density->setVal((double)random(600,800)); - } + airQuality->setVal((airQuality->getVal()+1)%6); // simulate a change in Air Quality by incrementing its current value by one, and keeping in range 0-5 + o3Density->setVal((double)random(200,500)); // change the Ozone Density to some random value between 200 and 499. Note use of (double) cast since random returns an integer. + + // Note we are NOT updating the Nitrogen Dioxide Density Characteristic. This should therefore remain steady at 700.0 } // event }; ////////////////////////////////// + +// WHERE ARE THE READINGS FOR the AIR Quality Sensor DISPLAYED? +// +// diff --git a/src/Services.h b/src/Services.h index 1ffd9d4..d89c04d 100644 --- a/src/Services.h +++ b/src/Services.h @@ -89,6 +89,8 @@ namespace Characteristic { struct ColorTemperature : SpanCharacteristic { ColorTemperature(uint32_t value=50) : SpanCharacteristic{"CE",PR+PW+EV,(uint32_t)value}{} }; + struct CurrentTemperature : SpanCharacteristic { CurrentTemperature(double value=0) : SpanCharacteristic{"11",PR+EV,(double)value}{} }; + struct FirmwareRevision : SpanCharacteristic { FirmwareRevision(char *value) : SpanCharacteristic{"52",PR,(char *)value}{} }; struct Hue : SpanCharacteristic { Hue(double value=0) : SpanCharacteristic{"13",PR+PW+EV,(double)value}{} }; @@ -139,7 +141,7 @@ namespace Characteristic { struct SwingMode : SpanCharacteristic { SwingMode(uint8_t value=0) : SpanCharacteristic{"B6",PR+PW+EV,(uint8_t)value}{} }; - struct CurrentTemperature : SpanCharacteristic { CurrentTemperature(double value=0) : SpanCharacteristic{"11",PR+EV,(double)value}{} }; + struct TemperatureDisplayUnits : SpanCharacteristic { TemperatureDisplayUnits(uint8_t value=0) : SpanCharacteristic{"36",PR+PW+EV,(uint8_t)value}{} }; struct Version : SpanCharacteristic { Version(char *value) : SpanCharacteristic{"37",PR,(char *)value}{} };