While I haven't been actively working on this for a while, I have been thinking about the software design from time to time.
I've always imagined a paradigm of having a thread react to inputs from the PIRs, the rotary encoders, and the switches on the rotary encoders but putting things onto some kind of queue. Another thread would manage state and process items on the queue. That seems grand, indeed, though it might mean getting pretty far along in the Espressif SDK native features.
It so happens that for another little project I decided to finally take a look at esphome.io. That project makes it pretty easy to do simple sensor readings and have them feed into MQTT and/or https://www.home-assistant.io/. I wondered if it were worth the trouble, and I was overwhelmingly pleasantly surprised. It's well-documented, easy to work with, has good tooling, and it already supports zillions of things you might hook to your esp8266 or esp32 board. For things it doesn't directly support, it's often simple to add them as custom components, and for a lot of natively-supported things you can escape into Arduino-esque C++ to get exactly what you want. There's a vibrant community of people doing things with it, so it's not just "some guy" that you're depending on to have all of the ideas. The bonus is that your can do over-the-air (OTA) updates to the firmware after the first USB-connected upload. Pretty cool.
OK, that's great for scattering bme280s around your house, but can it be used for something as *cough* sophisticated as this project? I believe it can. It still has that simple-minded Arduino loop() inside, but it packs a lot into it while still iterating that loop pretty fast. I was afraid the busy loop could miss input events, but so far it seems to be fast enough.
My plan is to let esphome handle the low-level dealings with the inputs and outputs. I'll use the equivalent of a state table to react to the processed inputs and tell esphome what to do with the outputs. I'm back to breadboarding things to prove in individual components one at a time to make sure they actually behave in the way I expect. So far, so good.