This is probably something only a proper software engineer knows, but there are times when you just know your code is not right.
One of those times is when, during the testing, you find you are having to code for each edge case specially.
Now, don't get me wrong, some code has to have code for special edge cases. Edge cases are the boundary definition of any system. They happen. Error case are always a problem, but this is edge cases or "normal" operation.
I don't like them and they are always a clue of bad design. I had an edge case in my door logic that had to be patched three times. That is a massive clue I have the design wrong.
I designed some new logic in my sleep, and had to get up and document it at 3am else I would never get back to sleep. Coding / design in my sleep is not new. Try and convince HMRC of that for R&D tax credit claims though?!
This evening I implemented the design and it was almost perfect - minor change of some ordering, but it avoids those annoying edge cases even cropping up. It makes for much "cleaner" code, which is a clue you have it right. In some ways the logic that beauty is simplicity is correct applies to code as much to quantum physics!
As ever, the trick is modelling reality. I have locks that take time to engage or disengage, so model that and model the state of the lock based on the outputs, timers, and the feedback for lock engaged input if we have one. That allows me to know the state of the lock(s) and use that as an input to defining the state of the door.
Yes, traditional state machines are entirely driven on state and event, but if you can make state from status, that makes for a no-brainer state machine and is usually nicer.
The result looks good, and I have reduced the number of door states as a consequences, which is always a good sign.
For a change it was documented (at 3am) and revised as I coded. My test door / lock was helpful before making live on my home alarm system...