Completed Example 13

Fully documented Garage Door Opener and motorized Window Shade examples.
This commit is contained in:
Gregg 2020-08-18 20:36:40 -05:00
parent 6c5a5835e6
commit 7fd21f2bed
2 changed files with 87 additions and 34 deletions

View File

@ -5,6 +5,8 @@
// ------------------------------------------------ // // ------------------------------------------------ //
// // // //
// Example 13: Target States and Current States // // Example 13: Target States and Current States //
// * implementing a Garage Door Opener //
// * implementing a motorized Window Shade //
// // // //
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@ -14,6 +16,37 @@
void setup() { 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); Serial.begin(115200);

View File

@ -5,15 +5,15 @@
struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener
SpanCharacteristic *current; SpanCharacteristic *current; // reference to the Current Door State Characteristic (specific to Garage Door Openers)
SpanCharacteristic *target; SpanCharacteristic *target; // reference to the Target Door State Characteristic (specific to Garage Door Openers)
SpanCharacteristic *obstruction; SpanCharacteristic *obstruction; // reference to the Obstruction Detected Characteristic (specific to Garage Door Openers)
DEV_GarageDoor(ServiceType sType=ServiceType::Regular) : Service::GarageDoorOpener(sType){ // constructor() method DEV_GarageDoor(ServiceType sType=ServiceType::Regular) : Service::GarageDoorOpener(sType){ // constructor() method
current=new Characteristic::CurrentDoorState(1); current=new Characteristic::CurrentDoorState(1); // initial value of 1 means closed
target=new Characteristic::TargetDoorState(1); target=new Characteristic::TargetDoorState(1); // initial value of 1 means closed
obstruction=new Characteristic::ObstructionDetected(false); obstruction=new Characteristic::ObstructionDetected(false); // initial value of false means NO obstruction is detected
Serial.print("Configuring Garage Door Opener"); // initialization message Serial.print("Configuring Garage Door Opener"); // initialization message
Serial.print("\n"); Serial.print("\n");
@ -22,14 +22,16 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener
StatusCode update(){ // update() method 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"); LOG1("Opening Garage Door\n");
current->setVal(2); current->setVal(2); // set the current-state value to 2, which means "opening"
obstruction->setVal(false); obstruction->setVal(false); // clear any prior obstruction detection
} else { } else {
LOG1("Closing Garage Door\n"); 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); current->setVal(3); // set the current-state value to 3, which means "closing"
obstruction->setVal(false); obstruction->setVal(false); // clear any prior obstruction detection
} }
return(StatusCode::OK); // return OK status code return(StatusCode::OK); // return OK status code
@ -38,20 +40,23 @@ struct DEV_GarageDoor : Service::GarageDoorOpener { // A Garage Door Opener
void loop(){ // loop() method 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; return;
if(current->getVal()==3 && random(100000)==0){ 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); current->setVal(4); // if our simulated obstruction is triggered, set the curent-state to 4, which means "stopped"
obstruction->setVal(true); obstruction->setVal(true); // and set obstruction-detected to true
LOG1("Garage Door Obstruction Detected!\n"); 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; return;
if(target->timeVal()>5000) // This last bit of code only gets called if the door is in a state that represents actively opening or actively closing.
current->setVal(target->getVal()); // 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 } // 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 struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Shade with Hold Feature
SpanCharacteristic *current; SpanCharacteristic *current; // reference to a "generic" Current Position Characteristic (used by a variety of different Service)
SpanCharacteristic *target; 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 DEV_WindowShade(ServiceType sType=ServiceType::Regular) : Service::WindowCovering(sType){ // constructor() method
current=new Characteristic::CurrentPosition(0); current=new Characteristic::CurrentPosition(0); // Windows Shades have positions that range from 0 (fully lowered) to 100 (fully raised)
new SpanRange(0,100,10); new SpanRange(0,100,10); // set the allowable current-position range to 0-100 IN STEPS of 10
target=new Characteristic::TargetPosition(0); target=new Characteristic::TargetPosition(0); // Windows Shades have positions that range from 0 (fully lowered) to 100 (fully raised)
new SpanRange(0,100,10); 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("Configuring Motorized Window Shade"); // initialization message
Serial.print("\n"); Serial.print("\n");
@ -79,11 +84,20 @@ struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Sha
StatusCode update(){ // update() method StatusCode update(){ // update() method
if(target->getNewVal()>current->getVal()){ // The logic below is based on how HomeKit appears to operate in practice, which is NOT consistent with
LOG1("Raising Shade\n"); // 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 } else
if(target->getNewVal()<current->getVal()){ if(target->getNewVal()<current->getVal()){ // if the target-position requested is less than the current-position, simply log a "raise" message
LOG1("Lowering Shade\n"); 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 return(StatusCode::OK); // return OK status code
@ -92,15 +106,21 @@ struct DEV_WindowShade : Service::WindowCovering { // A motorized Window Sha
void loop(){ // loop() method void loop(){ // loop() method
if(current->timeVal()>1000){ // Here we simulate a window shade that moves 10% higher or lower every 1 second as it seeks to reach its target-position
if(target->getVal()>current->getVal()){
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); current->setVal(current->getVal()+10);
} else } else
if(target->getVal()<current->getVal()){ if(target->getVal()<current->getVal()){ // else decrease the current-position by 10 if the target-position is less than the current-position
current->setVal(current->getVal()-10); 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 } // loop
}; };