Hello there!

This is a site for stuff I write that doesn’t fit in 280 characters or simple images.

A lot of the posts here are AI-written. If your reaction is “ai:dr”, I don’t blame you. “I” post this stuff in the hope that humans and agents who find it later won’t have to do quite so much work—and that it inspires people to fix annoyances of their own, small or large.

Fixing the Home Assistant integration for my Dreame L50 Ultra

I wanted my Dreame L50 Ultra in Home Assistant, with its settings and room controls available for my own automations. That led to a small maintenance fork, an investigation into what the phone app sends, and a surprisingly consequential missing @property decorator.

I directed the work, with Codex doing the investigation, code changes and regression tests. It also helped draft this post. My role was setting the goals and providing the context from my vacuum and Home Assistant setup. The work described here was done in September.

The starting point was Tasshack’s Dreame integration. It already does the substantial work: connecting to the vacuum, exposing its settings, handling maps, and providing room-cleaning controls. My fork builds on its development branch, starting at v2.0.0b25, and maintains the changes on main.

Cleaning up after failed setup

A failed setup attempt can still have created connections, background workers, listeners, and timers. Home Assistant may try again, so those resources need to be cleaned up before another attempt adds more.

BigKelLearns’ cleanup pull request and the associated upstream issue were useful starting points. The upstream snapshot already cleaned up ordinary update failures. Authentication failures and cancellation needed more attention.

Cancellation was the tricky part. Cancelling an asynchronous task doesn’t stop work it has already started in a thread. Disconnect immediately, and that thread can still be creating the very resources you’re trying to close.

Codex changed the cleanup to wait for that work before disconnecting. Failed setup and normal unload now use a shared shutdown routine, and pending callbacks are stopped from scheduling more updates afterward. Cleanup errors are logged without hiding the original setup error.

The regression suite includes a hundred failed setup attempts using real worker threads and timers with mocked network responses. It checks that workers stop, sessions close, and the old objects can be collected. Selected tests also failed against the unpatched code in the ways the investigation predicted. That gave us something more concrete than a tidier-looking cleanup function.

Checking what my L50 actually supports

“L50 Ultra” is the name on the box, but the integration deals with internal model identifiers and firmware capabilities. In the September investigation, account metadata identified mine as dreame.vacuum.r9493h, running firmware build 6000.

Codex compared the integration’s settings with the configuration and protocol mappings in that model’s Dreamehome app plugin. That covered things like wash-water temperature, silent drying, hair compression and obstacle crossing. Most of the settings checked were already supported. The shared plugin serves other models too, so a feature name appearing somewhere in its code wasn’t enough to say my vacuum supported it.

There was a general metadata bug to fix as well: a device’s key index was checked against the capability table instead of the separate key table it actually indexed. Codex corrected the bounds check and added regression cases.

The missing decorator

The room-order investigation found a particularly small bug. The integration supports an older room-order format and a newer one, with existing logic to choose between them. But cleaning_sequence_v2 was missing its @property decorator.

The caller expected a boolean. Instead, it got a bound method, which evaluates as true. That forced the newer format even when the selection logic should have chosen the older one.

Adding the decorator restored that logic. Tests cover the modern L50 path and the older fallbacks. This was a reproduced code defect found during the audit; I hadn’t observed it failing on the physical vacuum.

A small maintenance fork

The result is a fork with a few specific defects fixed, building on the much larger integration Tasshack and its contributors made possible. The validation notes and L50 audit record the checks and the remaining hardware work.

The lifecycle tests use mocked network responses, and the app comparison didn’t send cleaning commands or change device properties. Longer-term reconnect behavior and resource use during real cloud outages still need checking. The fixes have regression coverage; that isn’t the same as proving every model, firmware version and failure path on hardware.

I’m glad we could turn this investigation into fixes other people can inspect and reuse. If you’re working through similar setup failures or model-support questions, the fork and audit notes should give you somewhere to start.

12:00 am / Home assistant , Dreame , Debugging , Codex , Ai assisted

Milkdo: the grocery bot Yrenia and I keep fixing

Yrenia and I use a Telegram bot called Milkdo to maintain our Todoist grocery list. We can send a message or a voice note in our group instead of opening Todoist to enter each item. It’s a small tool we actually use, and we keep fixing the parts that annoy us.

I first had a go at this myself in 2024, cobbling the code together by hand with some help from GitHub Copilot autocomplete. Much later, I brought it forward with OpenAI Codex doing substantial refactoring, bug investigation, implementation and tests under my direction. Codex also helped draft this post. This is a codebase that went from lightly AI-assisted to heavily agent-refactored over more than two years of use and maintenance.

I don’t think of Milkdo as a “claw.” It’s our grocery bot, with a specific list and a small set of operations. The point is to make maintaining that list less annoying for the two of us.

My first attempt, back in 2024

The first commit was on May 8, 2024. The codebase started as Dialoh, a bot for conversations with Ukrainian-speaking friends. I wanted to deploy it on Cloudflare and use its serverless services with Telegram. On May 9 I was working through webhooks, voice messages and transcription; by May 12, there was a working Durable Object integration to keep conversation history.

[... 746 words]

12:00 am / Telegram , Todoist , Cloudflare , Automation , Github copilot , Codex , Ai assisted

Solving my Dreame L50 Ultra's hairball problem with a 3D-printed adapter, wet/dry vac, and Home Assistant

The green Stanley adapter rests loose on the left inside the dock cavity; the DeWalt adapter is installed on the right and connected to its hose.

The Stanley adapter is resting loose in the dock cavity on the left. The DeWalt adapter is currently connected on the right. The DeWalt CAD model is shown below.

My Dreame L50 Ultra could pick up dog hair, but its dock struggled to get the dense wads of hair out of the robot and into the bag. I made an adapter so an external wet/dry vacuum could do the emptying instead, with Home Assistant handling the switching. The project is finished and in use, and I haven’t had a hairball since!

I directed the project, took measurements, and printed and fit-tested the parts. Codex did the CAD implementation and Home Assistant automation work, and helped draft this post.

The replacement bags annoyed me too. I was replacing them almost every week. Three hairy doggies shed a lot! Bags like these on Amazon were about fifty cents to a dollar each. Getting them cheaper on AliExpress is an option, but it still sucks when a hairball gets stuck and the bag is only half full. Throwing away a half-full disposable bag means more waste going to landfill, on top of paying for capacity I wasn’t getting to use. Cheaper bags wouldn’t fix the dock’s inability to get the hair into them.

[... 989 words]

12:00 am / Dreame , 3d printing , Home assistant , Build123d , Codex , Ai assisted

Introducing d2-rs: a Rust port of D2 for the CLI and WebAssembly

I’ve released d2-rs, a Rust port of the D2 diagramming toolchain. It includes the compiler, Dagre, ELK and TALA layout engines, SVG rendering, PNG/PDF/PPTX/GIF exports, and a command-line app. It also builds as a WebAssembly package for Node and browsers, which was one of my original reasons for wanting the port.

I directed the project over about two months, from September to early November. Codex did most of the investigation and implementation for the first six weeks. Then Claude supervised Codex runs, reviewed changes and chose the next work, as well as contributing implementation. The port follows D2’s source and uses its test suites to check the results.

The current release targets Go D2 master at 6a7b48378, from October 11. All 944 boards in upstream’s end-to-end golden suite match: 933 SVGs and 11 expected errors, across all three layout engines. That comparison normalizes the XML declaration, version stamp and trailing newline; the rest of the SVG must match byte for byte.

You can install the command-line app from crates.io:

cargo install d2-rs
d2-rs input.d2 output.svg

Or install the WebAssembly package, which follows D2.js’s compile / render API shape:

npm install @d2-rs/wasm

It’s an independent, unofficial project, licensed under MPL-2.0 like D2. All three layout engines run in process.

[... 1,606 words]

12:00 am / Rust , D2 , Compatibility , Codex , Claude , Ai assisted

A daily repair job for the filament tracker

I added a daily Codex job to check the filament tracker. When it finds a problem it can diagnose clearly, it can fix the code, validate the change, deploy it, and check the live site again.

I set the requirements and gave it permission to make scoped repairs. Codex does the investigation, implementation, and deployment. Codex also drafted this post from the recorded runs and my direction.

The stock scraper already runs hourly. This is a separate maintenance check: are the pages fresh, are products missing, do the history charts still connect to the current inventory, and do the bulk-sale markers agree with Bambu’s promotion data? It checks all eight regional pages and opens charts in a browser too.

Two runs show why I wanted this.

Whitespace broke 41 charts

On September 18, the daily check found 41 Japanese variants whose current rows no longer matched their history keys. The storefront’s product categories had trailing or duplicate whitespace. The inventory was there, but the chart lookup was broken.

Codex changed the report generator to normalize Unicode and whitespace in the history keys and put the resolved key on each row. It committed and pushed the fix, deployed through the project’s deployment script, and ran the production job.

The live check found no current variants missing history. It also opened a repaired Japanese PLA Silk+ chart and a Global Ash Gray chart, and checked that the regional pages were fresh and below the 2 MB size limit.

Blue became Reflex Blue

On October 12, the check found another split in Japan’s history. PETG Basic Blue had become Reflex Blue, but it still had color code 30603 and the same packaging. Matching by the displayed name made one product look like two separate histories.

Codex added a narrow alias for that rename, committed and pushed it, validated it locally, and deployed it. The stored history records were preserved. Afterward, the spool and refill charts each had one continuous 723-point series spanning September 12 through October 12. Codex checked the live Japan and Global charts as well.

These are small fixes, but they are the sort of maintenance I would otherwise have to notice and sit down to investigate. The daily job gives Codex a reason to look before I happen to open the affected chart.

The checks still have limits. Bambu’s regional promotion pages sometimes redirect to the US page, so Codex reports that it cannot independently verify each region’s bulk-sale rules. If it cannot establish a clear fix, the job is instructed to report the problem for me to follow up.

12:00 am / Bambu , Filament , Automation , Codex , Ai assisted

Building an Android app for spoken radar alerts on my scooter

The Garmin Varia RVR315 is designed as a bike radar, but I like using it on my scooter to help keep track of vehicles approaching from behind. The radar clips onto the back of my CamelBak with an EFEALL strap mount, and my phone stays in my pocket. I’m not a fan of mounting the phone on the scooter, so I wanted spoken information about vehicles approaching from behind without having to look at a screen.

The radar sends its detections over Bluetooth. I also have the recurring problem of Garmin’s Android app stopping in the background and me forgetting to open it again. I wanted clearer connection behavior as well as the spoken alerts.

The clip is sold as a saddle-bag strap mount with a Garmin quarter-turn fitting, including support for the RVR315. It can rotate and tilt and comes with a safety tether. I’m using it on my CamelBak instead of a bike’s saddle bag.

That turned into RideWatch, a standalone Android app I built with Codex doing the research and implementation under my direction. The source is now public. It’s still a prototype, but I’ve been using it on scooter rides.

It’s neat how much I can follow by listening. I can tell when a car behind me is slowing down to pull over or park, or when one is approaching very quickly. Those changing alerts give me a sense of what’s happening behind me while my phone stays in my pocket.

[... 916 words]

12:00 am / Android , Scooter , Bluetooth , Codex

Guard Dog and Sentry: watching doggies with Reolink on Android TV and Mac

The house I’m in has an extensive Reolink camera system, inside and outside. We’re keeping an eye on three doggies as they move between those spaces. Reolink’s desktop app didn’t automatically switch to the cameras with activity, and I found it a bit crashy in my use. I wanted the views to follow wherever the dogs were hanging out without having to keep switching cameras myself.

So I built two sister apps with the assistance of Codex:

  • Guard Dog, a Kotlin app for the living-room Android TV, gives everyone in the room a view of what’s happening.
  • Sentry, a native Swift app for my Mac, lets me watch the doggies while I’m working, including in Picture in Picture.

I supplied the goals and directed the work; Codex did the investigation and implementation described here. Codex also drafted this post.

Choosing the views

Both apps use local Home Assistant activity events to choose which cameras to show, with a direct event connection for one standalone camera. Video goes straight from the cameras or NVR to the viewing app. Home Assistant supplies the activity information without carrying the video.

Up to four cameras appear at once. Already-visible cameras keep their positions when another view arrives, and a cleared detection gets twenty seconds of grace. When everything goes quiet, the last active camera stays on screen. A dog briefly falling out of the camera’s detection zone shouldn’t make the whole layout collapse and reform.

[... 859 words]

12:00 am / Android , Macos , Swift , Reolink , Home assistant , Video , Codex , Ai assisted

Recovering readable C++ from Reolink's Apple Silicon SDK

I wanted to have a go at making my own ARM64 port of Reolink’s macOS app. They had not released one yet, and getting there first would have been fun to show off. Spoiler alert: they beat me. Oh well.

I’m doing this with the assistance of Codex. I supplied the goals and directed the work; Codex did the reverse engineering, implementation, and comparison tests described here.

I was also curious why an Electron app was still waiting for an ARM64 version. It turned out there was much more native code involved than that description suggested. The interface uses Electron, but communication with Reolink’s cameras and NVRs goes through a native C++ SDK, with its own device state transitions, encryption, media handling, configuration serializers and callback machinery. Porting that meant understanding considerably more than the interface.

Reolink beat me to releasing an ARM64 version. The work has continued, now using the official Apple Silicon binaries as a reference for reconstructing readable C++.

I still want a thorough understanding of what Reolink’s hardware and SDK can do, and a way to investigate some of the crashes in the official macOS app. Understanding the implementation might make those crashes easier to diagnose and, hopefully, fix.

A readable reconstruction could also help with Home Assistant integrations and custom apps. That connects to my Home Assistant camera-announcement work and the Guard Dog Android TV app and its Swift macOS sister app, Sentry. Understanding the native SDK could help explain device behavior and expose capabilities useful to those projects, even while the replacement is unfinished.

Recovering one small method can expose enough of the surrounding contract to make the next one approachable. One of the recurring problems is deciding what “recovered” actually means. A function can return the expected number while getting ownership, callbacks or object layout wrong in ways that only show up later.

C++ symbols help more than I expected. When the binary exposes a mangled name, it provides clues about the class, namespace, arguments and qualifiers. Codex reconstructs declarations that make the compiler naturally emit the same name. A convenient exported alias can keep a boundary callable, but it leaves unanswered questions about the actual declaration behind it.

The tests need to follow those questions. For a configuration callback, I care about the exact XML bytes and response fields. For encryption, the output buffer and error handling matter alongside the return value. For an object destructor, I need to know which resource is released, which state is cleared and which object owns the final deletion.

One recent example was the encryptor’s lifecycle. Codex recovered the RSA resource ownership and separate destructor variants to replace a minimal destructor with ordinary C++. Constructors had their own initialization and layout checks. Those are small-looking functions with consequences beyond whether the immediate test call crashes.

Device opening is more involved. Codex reconstructed and compared portions of the policy and state-machine chain, including transitions through opening and login states. Some checks use fixture-owned callbacks or inert transport boundaries to exercise ordering and return mapping repeatedly. That proves the covered boundary contract; the real network implementation and a successful live device session need their own evidence.

Codex also compares compiler output with the original ARM64 instructions. That catches a different set of mistakes from behavior tests: missing branches, argument-order errors, unexpected memory writes or an incorrect calling convention. Compiler differences can remain even when a readable implementation has useful behavior and ABI evidence. The comparison notes record that drift. I want the implementation to stay readable, even when the remaining mismatch lowers its score.

By early October, the work had moved further into natural callback-operation classes and configuration dispatch, with focused runtime, symbol, object-layout and generated-code checks. Splitting implementations into source files and testing their default build path has been useful too. A standalone test can accidentally supply a helper that the actual replacement doesn’t own.

The replacement SDK is still unfinished. Partial builds have unresolved symbols, mocked boundaries remain, and successful local comparisons don’t establish a complete working transport or desktop client. I keep those categories separate when counting progress, because a tested wrapper around an unknown implementation is still an unknown implementation.

The useful output so far is a growing body of understandable C++, plus checks that explain why I believe particular pieces match. There’s plenty left to recover. At least the next investigation can start from an explicit contract, with the remaining unknowns written down.

12:00 am / Reverse engineering , Reolink , Cpp , Arm64 , Codex , Ai assisted

My security camera tells me when the laundry is done

I wish Reolink did more with the speakers in its cameras. They’re already powered, on the network, and sitting in useful places. They could double as little smart speakers for announcements, but that use doesn’t seem to get much attention.

I got these camera announcements working with the assistance of Codex. I supplied the ideas and directed the work; Codex did the technical investigation and implementation for my integration fixes and automations. The existing integrations are credited below. Codex also drafted this post.

I’d already opened an issue about adding speaker playback to the library behind Home Assistant’s official Reolink integration. I decided to have Codex investigate getting it working for my setup.

I wanted to know when the washer or dryer finished, even from across the house with the laundry-room door closed. There was already a Reolink camera with a speaker next to the stairs, near the bedrooms and a bit away from the machines. That seemed like a useful place for the announcements to come from.

First I needed Home Assistant to know when the appliances finished. Samsung had announced a $4.99/month plan for SmartThings API access, with free access being phased out starting in October. I wasn’t about to pay a stupid subscription for that. I used LocalThings instead: a free, open source integration that talks to the washer and dryer directly over the local network.

I still had to connect the appliances to SmartThings first to get LAN access working, and keep them registered there. LocalThings handles Home Assistant’s ongoing communication with them locally.

The official Reolink integration still lists two-way audio and text-to-speech as unavailable, so I used a custom integration.

I started with ha-reolink-talk, Matthew Holm’s integration based on joeblack2k’s Reolink Talk. It already provides camera media players for audio files and text-to-speech, including cameras behind an NVR. I had Codex investigate and fix a couple of problems with my setup in a fork.

The first was camera discovery. Codex found that a separate request to the NVR failed because of its self-signed HTTPS certificate. The error was caught, and discovery quietly fell back to channel 0. I’m still wondering how the hell this worked in the upstream author’s setup without hitting that certificate error. Meanwhile, Home Assistant’s official Reolink integration was already connected and knew which channels existed. Codex changed discovery to reuse that information, which got the right cameras to show up.

The audio framing needed fixing too. The encoded audio blocks weren’t being packaged the way the camera expected. Codex corrected the block metadata, padding, and stream framing, which got speech playing through the camera. Those fixes are in my fork and an upstream pull request.

Once playback worked, I had Codex implement the laundry automations. Each appliance’s LocalThings progress sensor reaching finish triggers an announcement through the camera by the stairs. Piper supplies the speech: “The washer has finished its cycle,” or the dryer equivalent.

I had Codex put together a few versions of the sound for me to try, including repeated flute announcements and a Samsung completion melody. With the appliance and the camera playing the Samsung melody out of sync, it was a confusing cacophony. I went with one short tone followed by speech instead. The washer gets a higher tone and the dryer a lower one. Codex rendered each tone and its spoken message together into one audio file, so the camera only has to start playback once.

The laundry automations are specific to this house, but the integration fixes are there for other Reolink owners to use. A camera speaker is a pretty convenient little announcement speaker when it’s already in the right place.

12:00 am / Home assistant , Reolink , Automation , Ai assisted , Codex

My wobbly VHS-Decode pipeline

I wanted to recover some VHS tapes. Somehow this turned into tapping a signal inside a Sony VCR, putting capture cards in a computer from the junk pile at work, spreading the processing across three machines, and asking Codex to help redesign an amplifier PCB.

I built this VHS pipeline with the assistance of Codex. I supplied the ideas, directed the work, and handled the physical hardware; Codex did essentially all of the technical investigation and custom coding for my setup. The pipeline builds on the existing projects credited below. Codex also drafted this post.

The usual way to digitize VHS is to connect a VCR’s video and audio outputs to a capture device and record them on a computer. The VCR reads the tape and turns its signal into a picture before the computer ever sees it.

For better results, you can look for a nicer VCR with a built-in time base corrector, or TBC, which helps correct playback timing errors that make the picture wobble. Unfortunately, the decks I was looking at were $$$. With ancient media having a moment again, it feels like everybody wants the same aging hardware.

VHS-Decode offers a more hardcore route. You still need a working VCR, but you tap the RF signal inside it, near the tape-head amplifier, before the machine turns it into ordinary video. This is not the antenna connector on the back. Suitable capture hardware records that earlier signal, and the computer reconstructs the picture, including time base correction, in software.

[... 3,323 words]

10:34 am / Vhs , Preservation , Codex , Automation , Ai assisted

Projects