From 7fd21f2bed7df56556fe622f4729fe0f4a5fbb78 Mon Sep 17 00:00:00 2001 From: Gregg Date: Tue, 18 Aug 2020 20:36:40 -0500 Subject: [PATCH] Completed Example 13 Fully documented Garage Door Opener and motorized Window Shade examples. --- .../13-TargetStates/13-TargetStates.ino | 33 +++++++ .../Expert/13-TargetStates/DEV_DoorsWindows.h | 88 ++++++++++++------- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/examples/Expert/13-TargetStates/13-TargetStates.ino b/examples/Expert/13-TargetStates/13-TargetStates.ino index 8198ce8..a63b446 100644 --- a/examples/Expert/13-TargetStates/13-TargetStates.ino +++ b/examples/Expert/13-TargetStates/13-TargetStates.ino @@ -5,6 +5,8 @@ // ------------------------------------------------ // // // // Example 13: Target States and Current States // +// * implementing a Garage Door Opener // +// * implementing a motorized Window Shade // // // //////////////////////////////////////////////////////////// @@ -14,6 +16,37 @@ void setup() { + // In Example 12 we saw how to implement the loop() method for a Service to continuously monitor our device and periodically report + // changes in one or more Characteristics back to HomeKit using setVal() and timeVal(). In that example we implemented passive sensors + // that operated independently and required no input from the user, which meant we did not need to implement any update() methods. + + // In this Example 13 we demonstrate the simultaneous use of both the update() and loop() methods by implementing two new Services: + // a Garage Door Opener and a motorized Window Shade. Both examples showcase HomeKit's Target-State/Current-State framework. + // For physical devices that take time to operate (such as closing a door), HomeKit Services typically use: + + // * one Characteristic that HomeKit sets via update() requests to HomeSpan, and that represent a desired target state, + // such as opened, closed, or in some cases a percentage opened or closed; and + + // * one read-only Characteristic that HomeSpan use to track the current state of the device in the loop() method, as well as + // report back changes to HomeKit using setVal(). + + // Not all HomeKit Services utilize the same Characteristics to define target and current states. Some Services use Characteristics + // that are specific to that one Service, whereas others use more generic Characteristics. The common theme seems to be that HomeKit + // guesses the actions a device is taking, and updates it tile's icon accordingly, by comparing the value of the target state + // Characteristic it sets, and the current state Characteristic it receives in the form of Event Notifications. When they are the same, + // HomeKit assumes the physical device has reached the required position. When they differ, HomeKit assumes something will be opening, + // closing, raising, lowering, etc. The details of this process for each Service is outlined in the HAP documentation, but beware + // the document is not always up to date with the lastest version of the HomeKit application. Sometimes a little experimenting and a lot + // of trial and error is required to fully understand how each Service responds to different combinations of Characteristic values. + + // As always, we won't be connecting our ESP32 to an actual garage door or window shade but will instead simulate their responses and + // actions for illustrative purposes. In some ways the code is more complicated because of the need to simulate values - it might be + // easier if we actually were connecting to a garage door or window shade! + + // Fully commented code for both of our derived Services can be found in DEV_DoorsWindows.h. These examples do not introduce any new + // HomeSpan functions or features. Rather we are combining everything learned so far into two reasonably complex Services. You may + // want to reference the HAP documentation for these two parent Services to fully understand the meaning of the different value settings + // for each of the Services' Characteristics. Serial.begin(115200); diff --git a/examples/Expert/13-TargetStates/DEV_DoorsWindows.h b/examples/Expert/13-TargetStates/DEV_DoorsWindows.h index d5506e3..6de2b46 100644 --- a/examples/Expert/13-TargetStates/DEV_DoorsWindows.h +++ b/examples/Expert/13-TargetStates/DEV_DoorsWindows.h @@ -5,15 +5,15 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener - SpanCharacteristic *current; - SpanCharacteristic *target; - SpanCharacteristic *obstruction; + SpanCharacteristic *current; // reference to the Current Door State Characteristic (specific to Garage Door Openers) + SpanCharacteristic *target; // reference to the Target Door State Characteristic (specific to Garage Door Openers) + SpanCharacteristic *obstruction; // reference to the Obstruction Detected Characteristic (specific to Garage Door Openers) DEV_GarageDoor(ServiceType sType=ServiceType::Regular) : Service::GarageDoorOpener(sType){ // constructor() method - current=new Characteristic::CurrentDoorState(1); - target=new Characteristic::TargetDoorState(1); - obstruction=new Characteristic::ObstructionDetected(false); + current=new Characteristic::CurrentDoorState(1); // initial value of 1 means closed + target=new Characteristic::TargetDoorState(1); // initial value of 1 means closed + obstruction=new Characteristic::ObstructionDetected(false); // initial value of false means NO obstruction is detected Serial.print("Configuring Garage Door Opener"); // initialization message Serial.print("\n"); @@ -22,14 +22,16 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener StatusCode update(){ // update() method - if(target->getNewVal()==0){ + // see HAP Documentation for details on what each value represents + + if(target->getNewVal()==0){ // if the target-state value is set to 0, HomeKit is requesting the door to be in open position LOG1("Opening Garage Door\n"); - current->setVal(2); - obstruction->setVal(false); + current->setVal(2); // set the current-state value to 2, which means "opening" + obstruction->setVal(false); // clear any prior obstruction detection } else { - LOG1("Closing Garage Door\n"); - current->setVal(3); - obstruction->setVal(false); + LOG1("Closing Garage Door\n"); // else the target-state value is set to 1, and HomeKit is requesting the door to be in the closed position + current->setVal(3); // set the current-state value to 3, which means "closing" + obstruction->setVal(false); // clear any prior obstruction detection } return(StatusCode::OK); // return OK status code @@ -38,20 +40,23 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener void loop(){ // loop() method - if(current->getVal()==target->getVal()) + if(current->getVal()==target->getVal()) // if current-state matches target-state there is nothing do -- exit loop() return; - if(current->getVal()==3 && random(100000)==0){ - current->setVal(4); - obstruction->setVal(true); + if(current->getVal()==3 && random(100000)==0){ // here we simulate a random obstruction, but only if the door is closing (not opening) + current->setVal(4); // if our simulated obstruction is triggered, set the curent-state to 4, which means "stopped" + obstruction->setVal(true); // and set obstruction-detected to true LOG1("Garage Door Obstruction Detected!\n"); } - if(current->getVal()==4) + if(current->getVal()==4) // if the current-state is stopped, there is nothing more to do - exit loop() return; - if(target->timeVal()>5000) - current->setVal(target->getVal()); + // This last bit of code only gets called if the door is in a state that represents actively opening or actively closing. + // If there is an obstruction, the door is "stopped" and won't start again until the HomeKit Controller requests a new open or close action + + if(target->timeVal()>5000) // simulate a garage door that takes 5 seconds to operate by monitoring time since target-state was last modified + current->setVal(target->getVal()); // set the current-state to the target-state } // loop @@ -61,16 +66,16 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Shade with Hold Feature - SpanCharacteristic *current; - SpanCharacteristic *target; + SpanCharacteristic *current; // reference to a "generic" Current Position Characteristic (used by a variety of different Service) + SpanCharacteristic *target; // reference to a "generic" Target Position Characteristic (used by a variety of different Service) DEV_WindowShade(ServiceType sType=ServiceType::Regular) : Service::WindowCovering(sType){ // constructor() method - current=new Characteristic::CurrentPosition(0); - new SpanRange(0,100,10); + current=new Characteristic::CurrentPosition(0); // Windows Shades have positions that range from 0 (fully lowered) to 100 (fully raised) + new SpanRange(0,100,10); // set the allowable current-position range to 0-100 IN STEPS of 10 - target=new Characteristic::TargetPosition(0); - new SpanRange(0,100,10); + target=new Characteristic::TargetPosition(0); // Windows Shades have positions that range from 0 (fully lowered) to 100 (fully raised) + new SpanRange(0,100,10); // set the allowable target-position range to 0-100 IN STEPS of 10 Serial.print("Configuring Motorized Window Shade"); // initialization message Serial.print("\n"); @@ -79,11 +84,20 @@ struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Sha StatusCode update(){ // update() method - if(target->getNewVal()>current->getVal()){ - LOG1("Raising Shade\n"); + // The logic below is based on how HomeKit appears to operate in practice, which is NOT consistent with + // HAP documentation. In that document HomeKit seems to support fully opening or fully closing a window shade, with + // an optional control to HOLD the window shade at a given in-between position while it is moving. + + // In practice, HomeKit does not appear to implement any form of a HOLD control button, even if you instantiate that + // Characteristic. Instead, HomeKit provides a full slider control, similar to the brightness control for a lightbulb, + // that allows you to set the exact position of the window shade from 0-100%. This obviates the need to any sort of HOLD button. + // The resulting logic is also very simple: + + if(target->getNewVal()>current->getVal()){ // if the target-position requested is greater than the current-position, simply log a "raise" message + LOG1("Raising Shade\n"); // ** there is nothing more to do - HomeKit keeps track of the current-position so knows raising is required } else - if(target->getNewVal()getVal()){ - LOG1("Lowering Shade\n"); + if(target->getNewVal()getVal()){ // if the target-position requested is less than the current-position, simply log a "raise" message + LOG1("Lowering Shade\n"); // ** there is nothing more to do - HomeKit keeps track of the current-position so knows lowering is required } return(StatusCode::OK); // return OK status code @@ -92,15 +106,21 @@ struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Sha void loop(){ // loop() method - if(current->timeVal()>1000){ - if(target->getVal()>current->getVal()){ + // Here we simulate a window shade that moves 10% higher or lower every 1 second as it seeks to reach its target-position + + if(current->timeVal()>1000){ // if 1 second has elapsed since the current-position was last modified + if(target->getVal()>current->getVal()){ // increase the current-position by 10 if the target-position is greater than the current-position current->setVal(current->getVal()+10); - } else - if(target->getVal()getVal()){ + } else + if(target->getVal()getVal()){ // else decrease the current-position by 10 if the target-position is less than the current-position current->setVal(current->getVal()-10); } } - + + // Note we do NOTHING if target-positon and current-position is the same - HomeKit will detect this and adjust its tile icon accordingly. + // Unlike the Garage Door Service above, we do not need to set any Characteristic telling HomeKit the shade is actually raising, lowering, or stopped. + // HomeKit figures this out automatically, which is very good, though unfortunately inconsistent with the HAP Documentation. + } // loop };