OpenSprinkler: When “Better” Has to Mean Something
Safer credentials, complete device support, correct schedules, and local control where failure gets wet
OpenSprinkler: When “Better” Has to Mean Something
Not in the HomeSeer store quite yet. It’s been running here every day, and the plan is to send it to the store as a beta.
After several projects involving undocumented APIs, reverse-engineered protocols, hidden headers, odd authentication flows, and devices occasionally interpreting standards as creative suggestions, OpenSprinkler felt almost luxurious.
It is an open hardware and open-source project.
The hardware designs are published.
The firmware source is available.
The local API is documented.
And you can talk directly to the controller over the local network.
No packet archaeology required.
There is an interesting wrinkle in my installation, though.
My controller is not an official OpenSprinkler-branded unit.
It is compatible hardware built from the published OpenSprinkler specifications and schematics and running the OpenSprinkler firmware.
Which is, really, a rather nice demonstration of what “open” is supposed to mean.
The useful part is not the logo printed on the enclosure. The design and firmware are open enough that someone else can build compatible hardware and still participate in the ecosystem.
After several reverse-engineering projects in a row, working with something that was actually intended to be understood felt almost suspiciously pleasant.
Replacing an old plugin without pretending it never existed
There was already a HomeSeer OpenSprinkler plugin.
It just had not been actively maintained in quite some time.
So this plugin is intended as a modern replacement.
But “replacement” does not mean:
Take over everything the previous plugin created.
It does not rename the old devices.
It does not silently convert them.
It does not decide that because I wrote a new plugin, I am entitled to reorganize somebody else’s irrigation automation.
Those HomeSeer devices may have years of events attached to them.
Migration is a user decision.
A plugin does not get to stroll into someone’s system, rearrange the sprinklers, and announce that everything is cleaner now.
Especially when the thing being rearranged controls water.
What “better” actually means
It is easy to say:
“This plugin is better.”
That tells you almost nothing.
Better how?
Faster?
Prettier?
More reliable?
More complete?
Has a more impressive collection of screenshots?
So I would rather make the claim concrete.
The controller credential is treated like a credential
The controller password is kept in the plugin’s encrypted secret storage, not sitting in a cleartext .ini file.
The OpenSprinkler API itself authenticates requests using a hash of the device password, but the larger point is simpler:
Secrets belong in secret storage.
That seems like a fairly modest standard.
It is still worth meeting.
All sixteen stations should actually work
This was one of the reasons I started looking more closely at the older integration.
My controller has 16 stations.
HomeSeer could see all sixteen through the old plugin.
But in my installation, only the first eight behaved properly through the integration.
Stations 9 through 16 were present, but they did not reliably report state or respond to HomeSeer commands.
There is an important qualification here.
I cannot say with certainty:
“The old plugin had an eight-station bug.”
Remember, my controller is not official OpenSprinkler hardware.
It is compatible hardware built from the published design and running OpenSprinkler firmware.
The controller itself operated all sixteen stations correctly.
Its schedules worked.
Its own interface could operate them.
The trouble appeared specifically in the combination of that hardware and the older HomeSeer integration.
Maybe the plugin made an assumption that held perfectly well on the official hardware it had been developed against.
Maybe my compatible controller differed in some subtle implementation detail.
Maybe the interaction exposed a genuine plugin limitation.
I do not have enough evidence to assign blame confidently, and I would rather not manufacture certainty for the sake of a cleaner story.
What I can say is:
Eight worked through HomeSeer. The other eight did not.
So the new plugin does not assume the answer.
It asks the controller.
The current API explicitly reports the number of station groups and the total station count.
If the controller reports sixteen stations, the plugin handles sixteen.
If it reports more, the architecture does not suddenly become surprised because someone once decided eight was probably enough.
The protocol gets the deciding vote.
Schedules should say what the schedule actually means
This was one of the more important correctness problems.
OpenSprinkler supports several different kinds of program scheduling.
A program can be:
Weekly.
Single-run.
Monthly.
Interval-day.
Those are fundamentally different schedule types.
An interval program might mean:
Every third day.
That is not the same thing as:
Saturday.
In the older integration, I found interval data being interpreted as though the underlying day fields were always a weekly weekday mask.
So something whose actual meaning was:
Every three days
could be displayed as a particular weekday.
The result looked perfectly plausible.
Which is exactly what made it dangerous.
Nothing crashed.
Nothing displayed nonsense.
The software simply supplied the wrong interpretation with complete confidence.
Those are my least favorite bugs.
If something says:
ERROR
you investigate it.
If something says:
Runs Saturday
you tend to believe it.
The current OpenSprinkler API explicitly identifies the schedule type and documents how the day fields change meaning with that type.
So that is how the new parser treats them.
Weekly means weekly.
Monthly means monthly.
Interval means interval.
An every-third-day program is displayed as every third day.
Reality gets to keep its original shape.
More than turning a zone on and off
OpenSprinkler exposes considerably more than:
Station 4: On
and I wanted the plugin to reflect the useful capability surface rather than stopping at whichever endpoints were quickest to implement.
That includes things such as:
Station state.
Program schedules.
Watering history.
Water-level adjustment.
Weather status and adjustment information.
Rain delay, including when the delay actually ends.
Pause and resume.
Stop all.
Controller state and health information.
The controller already knows these things.
I would rather expose them than have HomeSeer pretend they do not exist.
Rain delay should include “until when?”
A simple:
Rain Delay: Active
is useful.
But the next obvious question is:
Until when?
The API provides that too.
It reports both whether a rain delay is active and the epoch time at which that delay ends.
So the plugin can say more than:
Not watering because of rain delay.
It can say:
Not watering until this time.
That is much more useful both for automation and for the inevitable moment when someone asks why the lawn is not running.
Logs that answer the next question
The same philosophy applies to logging and health.
A message saying:
Operation failed
is technically information.
It also immediately creates another question:
Why?
And then:
What am I supposed to do about it?
So where the plugin can identify both the failure and a likely remedy, it says both.
The goal is not merely to document that someone now has a problem.
It is to help them get to the next useful step.
Written from the specification, then challenged by reality
The parser was written against the official firmware 2.2.1(5) API reference.
That is exactly how I would prefer to work.
There is a specification.
Wonderful.
And the current documentation is surprisingly detailed.
It defines station groups.
Station status.
Program types.
Interval schedules.
Weather adjustment.
Rain delays.
Pause state.
Logs.
Partial writes.
All the things an integration should not have to reverse-engineer if the manufacturer has already documented them.
But I still verified the behavior against the controller I actually own.
That matters particularly because mine is compatible hardware rather than an official unit.
In principle, it is exactly the sort of implementation an open hardware project should make possible.
In practice:
Compatible does not necessarily mean indistinguishable in every electrical and behavioral detail from every official hardware revision.
Tiny differences can exist.
And tiny differences are where software assumptions usually come out from behind the furniture.
So where the specification and the hardware agree, excellent.
Where they do not:
The live hardware wins.
And the difference gets documented.
The API reference tells me what the implementation is supposed to do.
The device on the network tells me what it actually does.
When I am about to operate a valve, I am sending the command to the device.
Not to the documentation.
A parser that does not know HomeSeer exists
I deliberately kept the parsing layer independent of the HomeSeer SDK.
It contains no HomeSeer dependencies.
That means the test suite can feed it captured OpenSprinkler responses directly and validate the results without needing:
A running HomeSeer installation.
A controller on the network.
Or an actual sprinkler valve outside deciding to participate in unit testing.
That separation matters.
Tests that require special hardware have a tendency to stop being run.
Maybe the hardware is somewhere else.
Maybe it is offline.
Maybe the test would change real state.
Maybe running it is simply inconvenient.
Eventually the integration test becomes something everyone assumes probably still works.
The parser tests have no such excuse.
Give them input.
Check the output.
Run them every time.
Reading is easy; writing deserves paperwork
Reading a controller is relatively forgiving.
Ask a question.
Parse the response.
Display the result.
Writing is different.
Writing changes reality.
So before I built restore functionality, I documented the write side separately.
Station names.
Attributes.
Per-board bitfields.
Special-station markers.
Which fields are safe to restore.
Which ones I deliberately do not restore yet.
That work happened before the restore code, not afterward.
Because there is an important difference between:
“The API accepts this field.”
and:
“I am comfortable restoring this automatically into somebody’s irrigation controller.”
The second deserves a higher standard.
The current partial-write rule matters
One especially useful characteristic of the current 2.2.1(5 API is that omitted parameters retain their existing value.
The station-change endpoint says the same thing explicitly:
Omitted station fields are unchanged.
That means a partial update really can be partial.
Rename one station without rewriting unrelated attributes.
Change one option without rebuilding the entire configuration from memory.
That is very useful.
It also deserves a date and version attached to the claim.
Older OpenSprinkler API generations had places where partial-write semantics were considerably less friendly.
So I do not want this code relying on folklore that:
“OpenSprinkler writes have always worked this way.”
What I can say is:
The current API explicitly documents this behavior, and the implementation is written against that contract.
That is much stronger.
Partial writes also create a restore trap
Safe partial writes have another side.
Imagine a restore operation that forgets a field.
The API does exactly what it was asked to do.
Every supplied value gets restored.
The omitted value remains whatever was already on the controller.
Nothing fails.
The request returns success.
And the restore looks complete.
Except it was not.
That is precisely the sort of failure I would rather think through while writing the restore specification than discover afterward while comparing two controllers and wondering why one obscure setting survived intact.
So restore code gets treated much more cautiously than ordinary state control.
Reading is easy.
Writing state deserves paperwork.
Why local matters particularly here
I prefer local control almost everywhere.
It is faster.
It is more private.
It has fewer dependencies.
And it keeps working when the Internet does not.
But irrigation gives local control another advantage:
The failure has physical consequences.
If a cloud-controlled light service disappears, a lamp may fail to turn on.
Annoying.
If a media service has a bad afternoon, movie night becomes technical support.
Also annoying.
Irrigation controls water.
Failure can mean:
The yard does not get watered.
Or:
The yard gets enthusiastically watered during the rainstorm you were specifically trying to avoid.
Water is a considerably less abstract failure mode than a missing status icon.
OpenSprinkler’s local API means HomeSeer talks directly to the controller sitting on the same network.
The Internet can disappear without the house forgetting where its irrigation valves are.
That seems exactly right.
The openness matters too
My controller makes another part of the philosophy unusually tangible.
It is not official hardware.
Someone was able to build compatible equipment from the published OpenSprinkler design and run the published firmware on it.
Years later, I can write a new HomeSeer integration against the published API.
Nothing about that requires finding an abandoned cloud service or convincing an obsolete mobile app to authenticate.
That is a form of longevity I appreciate more every year.
Open designs do not magically eliminate compatibility problems.
My own station experience proves that.
What they do provide is enough information to understand and solve them.
That is a much better place to start.
What I like about this one
Most of the work here was not about defeating the platform.
It was about being careful with a platform that was already willing to cooperate.
Ask how many stations really exist.
Make all of them useful.
Interpret schedules according to their actual type.
Expose the useful controller state.
Keep credentials protected.
Separate parsing from HomeSeer.
Test without requiring live hardware.
Write against the published protocol rather than a logo on the enclosure.
Verify against the device anyway.
And be appropriately cautious when the operation stops being:
Tell me something
and becomes:
Change something.
That last distinction feels especially appropriate here.
Software can afford to be cavalier when it is moving pixels.
It should be a little more thoughtful when it is moving water.