Hard To Integrate Is Not The Same As Secure

I have connected my Dogness T03 treat feeder to Home Assistant, and the most useful thing I learned along the way had nothing to do with dogs: a device that is hard to integrate feels secure, but those are different properties. The feeder gave me plenty of resistance. Very little of it was the kind that protects anybody. If you want to try it yourself, the add-on is on GitHub at matthewratcliffe/ha-dogness-t03, and you are welcome to install it and/or fork it.
Why bother
The motivation was plain. I wanted one less app on my phone. The feeder has a camera and a treat dispenser, and the manufacturer's answer to both is a dedicated app. That app is fine, but it is another login, another set of notifications and another place my household has to look. Everything else in the house already lives in Home Assistant. A treat button that sat beside the lights and the heating, rather than in a separate silo, was the whole brief.
The bigger prize is what comes after the button. Once the feeder is just another entity, it can take part in automations. Minnesota is an older dog who sleeps most of the day and moves for about an hour in total, so there is real appeal in letting the house react to her routine rather than me remembering to open an app. The automations are the reward at the end of this project rather than the project itself, and now that the plumbing works I can start doing more for my furry best friend.
How the feeder talks
The first surprise was how the device is meant to be reached. It does not expose a friendly local API. It is built on a peer-to-peer connectivity platform that is widely used in consumer cameras, and everything travels over UDP: the connection handshake, periodic keep-alives, control commands and the video itself, split across datagrams. The vendor app logs into the manufacturer's cloud, learns which device is yours and what key unlocks it, and then connects to the feeder.
That structure shaped the whole design. The cloud is needed once, to fetch the device identifier and key. After that, if the feeder and Home Assistant share a network, the add-on can find the device with a local broadcast and talk to it directly. Video and treat commands never need to leave the house. I care about that for the ordinary reason that a camera pointed into my living room should not relay through somebody else's servers when it does not have to.
The unglamorous consequences were networking ones. The feeder answers from unpredictable high ports, which bridged networking breaks, so the add-on needs host networking. Home Assistant itself then cannot reach that stream the way it would a normal camera, because the operating system firewalls its own core away from a host-networked add-on, which is why the add-on ships a small dashboard card that plays the video straight from the browser instead. None of this is elegant. It is what you get when you integrate with a device whose designers never imagined you would.
Getting in
A word on method first, because it matters. None of this was a break-in. Everything I used came out of the manufacturer's own app — the ordinary package anyone can download — read on hardware and an account that are mine. Every step was carried out against my own feeder alone; I never attempted to reach, enumerate or access a device belonging to any other owner, and I never touched the manufacturer's servers beyond the same login the app itself makes on my behalf. The protocol's "secrets" were not prised out of anything: the scrambling key, the fixed account name and my own feeder's key were sitting in the app's code or handed straight back by a normal login. I did not defeat the manufacturer's protection so much as read what they had left in plain sight and then do the same thing their app does, to the one device I own.
The way in started with the app, not the device. I pulled the Android package apart with jadx and apktool and read the camera controller, which was more candid than I expected. It held the account name the device logs in with, "admin", fixed for every unit; the licence key the vendor's native code wants before it will start; and the numeric control commands I would need, including the one that begins the video and the one that drops a treat.
The protocol itself, though, lives in vendor-supplied native libraries, so the next stop was Ghidra. The most useful thing I found there was not a secret but a mislabel. The login routine, when it dislikes your credentials, sets an internal flag and returns the error code the public headers call "timeout". I had been treating a rejected password as a flaky network for longer than I would care to admit; reading the disassembly turned a baffling dead end into an obvious one.
Before writing any clean code, I wanted to drive the real protocol against the real device, so I cross-compiled a tiny C bridge with the Android build tools and ran it on a phone, linked against the vendor libraries. Two initialisation quirks had to be satisfied first — the licence key set before anything else, and the local port pinned to the narrow range the SDK silently insists on — and then a sweep of candidate account names confirmed that only "admin" is ever accepted. The bridge connected, authenticated, started the stream, and dispensed a treat. That was the moment the project stopped being guesswork: I now had a known-good reference to imitate.
Then I captured the bridge's own traffic and rebuilt the obfuscation in a few lines of Python until my version matched the capture byte for byte. That is where the "encryption" revealed itself as a costume. The transform is keyed by a fixed string compiled into the software, so reproducing it needed no secret at all, only patience and a packet trace.
From a protocol to a dashboard
With the protocol understood I reimplemented it cleanly in Go, with no vendor code, by extending go2rtc — an open streaming server that Home Assistant already uses, and that happened to understand a related camera family's dialect of the same platform. Most of the work was pinning down how this model differs from that cousin, and the differences were almost comically small: it announces itself with one changed version byte, carries slightly different version bytes in the handshake, and uses one differently sized packet. Three edits, and go2rtc's existing media handling did the rest. Bringing it up against hardware shook out a handful of device-only bugs too — a socket handing back addresses in a format that never compared equal, so every reply was silently dropped; a handshake timeout that leaked into the video loop and killed the stream after five seconds; and the plain fact that nothing streams until you explicitly ask it to.
The cloud appears only once, to fetch what the device will not tell you itself. The login sends your password as a pair of salted MD5 hashes and gets back a token; the device list hands over the feeder's identifier and its key, but pointedly not its address on your network. So the add-on finds the feeder the local way, broadcasting a search on the platform's discovery port and matching the device that echoes the right identifier back. Nothing to configure, and it follows the feeder across address changes on its own. I packaged the result as a Home Assistant add-on that ships prebuilt binaries for each processor type, after an earlier version that compiled on the device proved far too heavy and hung the installer on a Raspberry Pi. From the owner's side, the only input is an email and a password.
Getting the picture onto a dashboard was its own small project. Because the add-on must use host networking, Home Assistant's core cannot reach the stream, so the browser does instead, through a card the add-on installs and registers for you. Making that card play well took three separate fixes. The frames first arrived with no timing information, so the player treated the stream as one enormous burst and sprinted through it; the cure was to read the device's own millisecond timestamp out of a trailer on each frame and pace playback by it. Even paced, the raw feed's uneven timing and occasional dropped frames left players stuttering and lurching, so the add-on now re-encodes it on the fly into a clean, constant-rate stream. And then, maddeningly, the card still would not connect, because the streaming server refused the browser for coming from a different port — a single over-strict origin check, undone with a single line of configuration. After that the live video finally played the way it should: smoothly, in real time, on the desktop and in the phone app alike.
Where the difficulty actually came from
This is the part I want to be careful about. The integration was not simple, and at first glance the feeder looks well defended: nothing documented, a proprietary protocol, payloads that are unreadable on the wire, and a version dialect that differs just enough from the nearest open-source implementation to make every early attempt fail quietly. It is natural to look at that and conclude the manufacturer took security seriously. My first impression was exactly that.
Having spent time inside it, I would not say that now. Most of what stood in the way was obscurity and undocumented behaviour rather than anything a cryptographer would recognise. The datagrams are scrambled, but the scramble is not encryption in the sense we normally mean, because it does not depend on a secret that only the two endpoints hold. The key is a fixed string baked into the software — in this family of devices it is the cheerful line "Charlie is the designer of P2P!!" — so anyone who can read the traffic can undo it. That is a disguise, not a lock. Access to the camera is then gated by a per-device key, which is a sensible shape on paper, except that the account name beside it is simply "admin" on every unit, and the key itself is handed back to you by the cloud the moment you log in. The platform underneath also has a public history of session-level vulnerabilities — CVE-2021-28372 let a remote attacker take over a device's connection — which matters for anything reachable from outside the home.
What actually ate my time, looking back over that process, was never a lock. It was a mislabelled error that sent me chasing a network fault that did not exist, a version byte this model happens to send differently from its cousins, and behaviour the vendor simply never wrote down — the treat command, for one, that quietly does nothing unless the camera is already streaming. None of those is a defence. They are the friction of a thing nobody outside the company was meant to open, and friction protects no one in particular.
So the honest summary is that the feeder was hard for me, and it would be hard for a casual attacker, but it would not necessarily stay hard for someone patient and motivated. Effort is a speed bump. It filters out the idle and the impatient. It does not filter out the determined, and it does not give a family good reason for confidence about a camera in their home. My full notes are in the repository's research write-up, written descriptively rather than as a how-to, for anyone who wants the detail.
What I would do as an owner
The practical lesson is to treat these devices according to what they are, not how they feel. The integration deliberately keeps everything local, which shrinks the exposure considerably, and the next sensible step is to stop the feeder reaching out to the internet at the router while leaving local access alone. The add-on does not need a cloud media path, so it keeps working. The credentials you enter stay in your own Home Assistant instance and go only to the manufacturer's login service. And if you ever paste a password somewhere careless while experimenting, rotate it afterwards.
There is a commercial reading as well. A manufacturer sells a treat feeder and an app, and it is rational to optimise for the app experience and the cloud relationship. Nobody sets out to make local integration miserable. But when a product's protection is mostly that nobody has looked closely, the cost lands on the customer later, and it lands on exactly the people who trusted the device in their home. I would rather see vendors publish a local interface and spend their effort on real encryption than rely on the awkwardness of their own protocol.
One less app
The result is modest and I am happy with it. There is a treat button and a camera in Home Assistant, the phone app can go, and the feeder is now something my house can respond to rather than something I have to remember. If you own the same model or a close relative, the repository explains installation, including adding it as a custom add-on repository, and forks are very welcome if your device behaves differently from mine.
The broader point travels well beyond pet feeders. When something is difficult to get into, ask who it is difficult for and why. Difficulty that comes from good design is worth trusting. Difficulty that comes from nobody having bothered to document the thing is just difficulty, and it is a poor basis for deciding what you let into your home.
A note on the "I"
I have written all of this in the first person because it is my project, my dog and my home, but it would be dishonest to let that stand unqualified. The large majority of the work here — the decompiling and disassembly, the protocol reconstruction, the code, and a good deal of this write-up — was done by Claude, running on Opus 4.8 with high reasoning, while I set the direction, tested against the real hardware, and made the calls. The judgement and the responsibility are mine; most of the actual labour was not. It seems only fair to say so in a post about not taking things at face value.
There is a fitting footnote to that. When it came time to publish the one-line change that loosened the camera server's origin check, Claude's own tooling refused to push it, flagging the edit as weakening security, and relented only once I explicitly approved it. A machine's guardrail pausing to ask a human before it published something that made a system a little less strict is exactly the kind of friction that is worth having — and an apt reminder, in a post about not trusting things blindly, that the useful question is always who a restriction is protecting and why.


Share your thoughts