Rotten Chicken & Structural Pattern Matching

soaring vulture over moody dark blue sky
Movement in the sky instantly caught my attention: vultures, about eight of them. Some soared in narrowing circles while others leered from the branches of the big cedar tree. They had the scent of something very compelling. I walked through the north gate next to my truck camper where I could smell it too. As this is a fairly high foot traffic area, I was surprised that something could get to that level of decay without somebody noticing.

I started searching around for the source. I strained to look under the camper for a dead animal. There was nothing there. Inside? How could that happen?

My adventure rig

Cautiously, I opened the door. A backdraft of stench shoved me away and brought my recent breakfast to my throat. Rallying and holding my breath, I ducked in and surveyed: nothing unusual to be seen. I looked at the refrigerator and, cringing, I opened the door. A dark brown goo spattered onto the floor from the dismayingly warm compartment.

I fled.

It turns out that the 12V power had failed and nobody noticed for weeks. Oddly, even when hooked up to grid power, the refrigerator needs the 12V power, too. The brown goo was what was left of a pair of formerly frozen organic chickens.

It was the most difficult cleaning job I've ever had. Every cleanser I tried failed to remove the smell: bleach, vinegar, lemon things, pine things, vanilla things. Baking soda doesn't seem to do anything at all. Activated charcoal filters with a circulating fan did no good.

You can support my
programming and writing
on Patreon or Ko-fi
Retirement is tough.
Even small
contributions help.

I almost gave up at this point. Replacement of the refrigerator would literally cost more than this worn out old camper was worth. In this condition, it was useless to anybody. I resigned myself to taking it to the landfill.

At a friend's suggestion, I acquired an ozone generator. I ran it in the camper with the refrigerator door open. It seemed to work on the camper interior itself, the upholstery, the bedding, but not the refrigerator. I repeatedly saturated the camper with ozone for hours at a time hoping that it would eventually work. It didn't.

I surmised that concentration of the ozone in the refrigerator itself would be more effective. However, the generator was not designed for such a tiny enclosed space. Its mechanical timer had a minimum setting of 15 minutes, I feared it would overheat.

I imagined cycling the power on and off for ninety seconds every half hour to start and stop the ozone generator. I cobbled together an experiment with webthings.io, a project spun out of Mozilla last year. It is IoT software that's both easy to program and YAML-free. I setup webthings.io on a RaspberryPi with a ZigBee adapter. In my work at Mozilla a few years ago, I wrote my own Python package to assist in controlling IoT devices (starting at: Things Gateway Series 2), so it was easy to throw together a rule.

At the same time as this project, I was looking at the advancements in the Python language introduced in version 3.10. I'm especially fascinated with Structural Pattern Matching. Python finally gets something akin to a case statement. It's a controversial feature for several reasons. It employs some syntactic surprises.

PEP 635
Structural Pattern Matching

more compelling
than horrifying

For me, the most notably disturbing idiosyncrasy is syntax that looks like a just like the invocation of class constructor. However in this special case, it is termed a destructuring instead. It appears that the Python interpreter selects if a given clause is an invocation of a constructor or a destructuring, solely by the context of the invocation. If it is inside the match clause of a case statement, it is a destructuring, elsewhere it is a constructor. The destructuring seems to reduce a class instance to a tuple based on selected values from the instance's attributes. That tuple is then used for pattern matching with the case statements. This is supposition on my part, I won't know for sure how this works until I really explore it. (See: from future import blog-post-about-this-topic)

I always enjoy diving head first into complexity and Structural Pattern Matching seems to be, well, a perfect match. I thought I'd try it out for the first time by using it for Event Dispatch in my ozone blaster IoT application. Of course, I was drawn to the obtuse destructuring variant of match patterns for my first effort. Identity matching is likely a more appropriate method for my application. However, my more complex implementation worked just fine.

The first task was setting up some timers. I needed a heart beat, an event that triggered every thirty minutes that would turn the ozone generator on. Then I needed a timer that would turn the generator off after ninety seconds. The whole program would run for twelve hours. Finally, during shut down, I needed a ten second timer to ensure that everything had time to shut down cleanly and ensure the ozone stopped before the program ended.

class RuleTrigger:
    __match_args__ = ("name",)

    def __init__(self, config, name):
        self.config = config
        self.name = name
        self.rules_that_use_this_thing = []
        self.canceled = False
  
(see this code in situ in the rule_triggers.py file in the pywot directory )

I wanted my Timers to have the destructuring behavior, so I gave the base class of my timer classes a __match_args__ class member. That ensured that my Timer instances would destructure into a tuple of the correct instant attributes. I wanted a tuple of only one value, the name that I assigned the timer during construction. I suspect there's something clever and fun that could be done here with name tuples. I'll explore that later.

I present my solution without comment. I'm curious if other people find the following code with its Structure Pattern Matching is as clearly readable as I find it to be. Alternatively, see the identity version of the Structural Pattern matching in my github repo.

class OzoneRule(Rule):

    def register_triggers(self):
        self.ozone_frequency = HeartBeat(self.config, "ozone_frequency", "30m")
        self.ozone_on_timer = DelayTimer(self.config, "ozone_on_timer", "90s")
        self.total_cycle_timer = DelayTimer(self.config, "total_cycle_timer", "12h")
        self.total_cycle_timer.start()
        self.end_of_cycle_timer = DelayTimer(self.config, "end_of_cycle_timer", "10s")
        return (self.heartbeat, self.ozone_on_timer, self.total_cycle_timer, self.end_of_cycle_timer)

    def action(self, the_trigger, the_event, new_value):
        match(the_trigger):
            case HeartBeat("ozone_frequency") if self.total_cycle_timer.is_running:
                self.ozone_switch.on = True
                self.ozone_on_timer.start()
            case DelayTimer("ozone_on_timer"):
                self.ozone_switch.on = False
            case DelayTimer("total_cycle_timer"):
                self.ozone_on_timer.cancel()
                self.ozone_switch.on = False
                self.end_of_cycle_timer.start()
            case DelayTimer("end_of_cycle_timer"):
                exit(0)
  
(see the identity version of this code in situ in the camper_rules.py file in the pywot rule system demo directory )

With this collection of hardware and software, I was 99% successful in eliminating the offending odor in the camper refrigerator, though it took five 12 hour runs. The camper refrigerator will probably never be entirely free of a slight scent of decay, aside from replacement, there's not likely anything I can do about it.

A cheap Amazon ozone generator, webthings.io, and Python's Structural Pattern Matching saved my camper for at least a few more adventures.

Expect to see more exploration of Python Structural Pattern Matching in a future post.