Smart Heating Registers
Joe Angell
Update: October 2020
I finally got fed up with the HomeBridge HTTP plug-in and switched to HTTP-Webhooks. This plug-in is newer, supports more device types, and has the capability of accepting pushes from the devices instead of constantly pulling. This significantly improved the stability of my registers, as they no longer got saturated by connections that required periodic rebooting, and it eliminated the ridiculous log files and port usage by the HTTP plug-in when the device couldn’t be reached.
Porting over was quite simple. Mostly I just had to install the new plug-in and set up the configuration file. The registers are mostly controlled through the Home app and associated automation anyway, so getting the state from the registers wasn’t a priority, although I did add it a few weeks later, and that was easy to implement as well. It now runs on its own Homebridge instance and the old plug-in is retired.
Update: August 2020
I’ve started adding more HomeKit wifi devices to my house, and decided to simplify this I created a new 2.4 GHz network with a unique SSID. This broke all the devices currently connected to that network, including the ESP8266-based heating registers. Since I had to pull them all to update the SSID, I also updated the stronger servos (which required a new mount) and did a small tweak to force the devices into station mode, which disables the FaryLInk access points that were littering the network previously. I’ve inlined these changes with the original build information below.
I managed to forget to update the firmware on one register when I changed out the servo and mount, and it took me far too long to notice that. I’ve since revised the firmware to blink the LED when it has no wifi connection, as well as fixing the LED on/off config option after something else changes the LED state, and set the servos to default to open all the way on power up. A bit later I updated it again to auto-reboot the device if the wifi was lost for more than five minutes, since that usually implies that it’s just never going to reconnect in its current state without being rebooted or power cycled. I also used the over-air updates for the first time to push these changes to the remaining registers, which worked remarkably well and was significantly faster than the USB connection, which was surprising.
A side effect of the stronger servos is that they can fairly easily break the pins on the printed servo arms if you’re not careful. Luckily I’d designed the arms with a hole for a 20mm M3 screw, which I wound up using a few times already when I broke the pins. The registers still partially work even with the pin broken, as gravity will drop the damper down on its own, and the arm can still lift it back up.
I’ve also added some basic information about using launchd to spawn Homebridge, segregating Homebridge plugins into separate instances for robustness, and kludging around a bug related to bad connections with homebridge-http by periodically restarting it with another launchd task. These are all in line with the rest of the post.
Our new house has forced air driven by an oil furnace. It has a total fo eight floor-mounted heating registers, all in a single zone. This mostly works, but at night it’s pretty noticeable how much less air the the three registers on the second floor get compared to the more numerous ones on the first. There are also times where heat seems to be going into rooms we’re not in, like if we’re eating the dining room versus watching TV in the den.
The obvious solution was to automate the heating registers. You can buy smart heating registers, but they only seem to be the flush-mount kind. I would have to build my own.
Identifying Return and Heating Registers
I’ve previously only had electric heat and A/C systems, so I assumed that all the registers put out heat. Only after I replaced all the first floor registers did Ir realize that the second run of ducting from the furnace was for return air — it was for the intake into the heating system. In my case, registers on the exterior walls (and one in the kitchen) emit heat, while the four in the middle of the house pull air back into the furnace, further heating it before sending it back out into the house.
The return registers are fairly easy to identify, as they usually don’t have dampers installed. I just thought they were broken, and made them smart. Luckily I found this out pretty quickly, and replaced them with proper return registers, meaning I took normal registers and removed the damper. In the case of the ones I bought, you just twist a small tab of metal with some pliers to pop out the damper lever, then slip the damper out of the hinges, and you’re done.
In total, my house has fifteen registers: three outlets on the second floor, eight outlets on the first floor, and four intakes on the first floor. A total of eleven registers would be converted to smart registers.
Design
Our registers are 18” long, so we went with white Accord Select registers from the Wicker Collection (part 231249). These are a simple sheet metal design painted white. The damper is adjusted with a lever that stops at specific detents. The face and damper pop off easily from the bottom with some moderate outward pressure.
I had a few design requirements, since I’d need to make a dozen of these. I wanted to use as few parts as possible, with as few wires as possible. I wanted to do the minimum number of modifications to the damper. Total assembly time is about 15 minutes per register, including calibrating the open and closed positions, at least once you get used to crimping the new connectors on the servo wires. Installation time is about 10 more minutes, including removing the old register, drilling the hole in the floor and tidying up the USB cables.
The easiest and cheapest way to control the damper is with a servo and an Arduino-compatible microcontroller with wifi, in this case a NodeMCU ESP8266. Mounts would be 3D printed in ABS plastic, although the temperatures are low enough that PLA should be fine. Power would be provided via the microcontroller’s USB port by drilling a hole through the floor under the register, sanding a 10’ or 25’ USB cable into the basement, and plugging it into one of three 6-port USB chargers mounted to the floor joists.
Materials Cost
The most expensive part of this project was the register itself. The prices shown are the cost per register, which is not necessarily the cost of each purchased item (such as in the case of the multi-port USB power supply).
I’ve included Amazon links, but I don’t know how long they’ll work. Prices are approximate as of March 2023. As an Amazon Associate I earn from qualifying purchases (learn more here), so using these links is a good way to help support this site.
Accord Heating Register, Part 231249: about $20. I bought these from Lowes after Amazon went out of stock.
NodeMCU ESP8266: $4 (5 for about $20) (Amazon)
MG90S Micro Servos, metal gears rated for 2 kg/cm at ~5v, 90-degree angle: They were 5 for $18 at the time, but the ones I originally used are no longer stocked. These are equivalent alternates, and about the same price.
10’ or 25’ USB Cable: around $2-$6 depending on length, and buying in four or five packs saves money. Note that these are charge-only cables, and cannot be used for data transfer, which is why they’re so inexpensive. (Amazon, 10’) (Amazon, 25’)
6 port USB Power Supply: $4 per register (around $16 for 6 ports, which I used to drive four registers). Unfortunately, the one I originally used no longer exists on Amazon, but this is similar. Note that this alternate may not fit the 3D-printed mount that can be found further down the page.
The average cost is about $35 to $40 per register. Not included in this cost are some connector ends, some small screws, and the ABS filament used to print the mounts.
Modifying the Register
There are two minor mods required for the register. To start with, the damper needs to swing freely, so we need to disable the detents. With the face plate off, I used a flathead screwdriver to bend the detent piece out so that it no longer contacted the bumps on the lever piece. The damper now swings freely, but won’t stay open on is own. That’s what we want, as a servo will control and hold the position.
The other modification is to drill a second center hole in the register body below the one already there. This is for further securing the 3D-printed mount for the servo and ESP8266 to the back of the register body. I used a stepped drill bit for this, which makes such drilling a lot easier than using one large bit, or having to constantly swap bits with successively larger ones.
For the second-floor registers, I was going to have to run the power cords from the sides instead of through the floor. For this, I drilled an extra hole in the side and ran the USB cable through that.
3D Printed Parts
By far the easiest way to make this work was with a few 3D printed parts.
Register mounts
There are three printed parts. I used ABS plastic, mostly because I had it and I didn’t have any PLA . The temperatures in the vents likely aren’t high enough to affect PLA, so that could be used as well.
The first part is the mount. After the register body is screwed onto the wall, the mount is screwed to the wall through the holes in the center of the register. This mount holds the ESP8266 and the servo. I designed this in modo as a block with an opening for the servo, a place to mount the NodeMCU, and a flange for screwing it into the wall through the register body. I tried to model in open spaces to reduce the amount of material and to lock as little airflow as possible, since this would be positioned directly in the center of the duct. I used digital calipers/micrometers to measure offsets to bypass flanges and the like on the back of the register, and to make sure that the mount wouldn’t interfere with anything when installed. The servo and microcontroller both sit inside the duct, thus allowing for free movement of the damper.
The second part is the servo arm, or horn. I designed one that would fit tightly on the servo’s spindle, and would be as vertical as possible when the damper is fully open by adding a slight bend to it. This screws onto the spindle with a normal servo screw, and has a pin coming off of it that attaches through a slot in the damper hook.
The damper hook is the final piece. This hooks onto the top of the back of the damper and then snaps onto the bottom edge, and provides a track that the pin on the arm arm can slide along. When the arm rotates, the hook is pushed or pulled to close or open the damper. I went through a few iterations until I got one that snapped firmly into place without sliding and without breaking.
All parts are sliced with Ultimaker Cura in the Standard quality, with for the hook and arm. I used an Sainsmart Creality Ender 3 Pro with black ABS filament to print them, although PLA would probably get fine — the hot air from the vents doesn’t get up to PLA’s glass transition temperature of 160 F. It takes four to five hours to print a full set of parts, and they’re small enough that multiple sets can be printed together. I printed extra arms, as the pins would tend to snap when disassembling the units if I wasn’t being careful enough. I also printed a few extra damper hooks.
A nice feature is that you can still manually close the damper, at least with the small servos. It takes more force, and the current state won’t show up correctly in the Home app, but it’s still an option if there is a problem with it. With the large servos you are more likely to break the arm and have to print another, but it’s still possible if you’re careful.
The above is the final design.
My original idea used two servos, one on each end of the register body, with two hooks on the damper. The left mount held the ESP8266 and one servo, while the other held only a servo. It was carefully designed to fit exactly inside the ends of the register without blocking the damper.
This worked, up until I tried to put it in the floor and found that the duct doesn’t actually extend to the edges of the register, but rather inset a good four or five inches. This is also when I switched from 9g/cm plastic gear to 2kg/cm metal gear servos — the extra torque allowed a single servo to be used. This doubled the price of the servo from about $2 to $4 per register, but halved the number of servos, so the net cost is the same. It also significantly reduced the amount of 3D printing needed.
I hit a snag on my ninth register install. For some reason, that duct was a little narrower than the others, and I couldn’t fit the assembly into the duct. I designed a new mount that held the NodeMCU behind the servo instead of perpendicular to it. This provides enough space that I could properly fit it in the duct. All the second-floor vents also required this thinner design.
USB Power Supply Bracket
I mounted three USB power supplies to the floor joists in the basement. It’s designed to sit on a desk, so I made a simple U-shaped bracket and printed it. It’s slightly tight, but it works fine for my purposes, and screws onto the joist with two sheetrock screws.
Larger Servo Design
These original servos worked fairly well, but they wouldn’t close or open the registers all the way. While I was updating them for a new wifi network, I decided to also upgrade to full-sized RC servos. These are 20 kg-cm, instead of the original 2 kg-cm servos, so, ten times as powerful. They easily open and close the registers completely, although these things aren’t airtight (and aren’t supposed to be), and you still get air through gaps in the sheet metal. This isn’t necessarily bad, as you definitely don’t want to block the off vents entirely and overpressure the system.
It only took me two or three test prints to get the redesigned mount working. I set it up so the servo spindle would be in about the same place as the original design, ensuring that mounting and the old hook would be compatible. I also had to print new arms to fit on the larger spindle.
3D Files
Here are the modo files and the generated STL files for printing.
modo LXO Files:
Mounts. Includes the standard and thin central mounts, and the original dual servo mounts, and the large servo mount.
Damper Hook and Servo Arm, including multiple variants as I tested designs for the center and original dual servo versions, and large and small servo arms..
STL files:
Assembly
Assembly is fairly straight forwards. There is some minor modification to the servo wires to remove the slack and connect them to the NodeMCU, install the software and calibrate them, mount them to the ducts, and run the USB power cables through the basement. The process is the same for both small and large servos.
Shortening the Servo Wires
Before doing anything else, I shortened the servo wires and added new connectors. This serves two purposes: it lets me put two new connectors that go to the correct NodeMCU pins in place of the existing three terminal connectors, and it avoids all the extra slack wire that I’d have to tie up and would otherwise block airflow and possibly make noise inside the register,
These connectors are referred to as “open barrel connectors”, and the pins inside “terminals”. You ou definitely want the correct crimp tool to do this. While you can do it all with pliers, it won’t be as clean and will take longer. Open barrel terminals crimp the wire and the insulation at two separate points on the terminal. If you don’t know how to use these, check YouTube for “crimp open barrel connectors” — there are a lot of videos, and it’s important that you do this correctly for a proper connection.
I had trouble keeping the wires from falling out of the terminals while I was getting them into the crimp tool in the first place. What worked for me was to place the stripped wire onto a terminal, then slightly squeeze the strain relief end of the connector around the insulation with a pair of pliers with just enough force to hold the wire in place. This makes it much easier to get the wire and terminal into the crimp tool itself. I then used the slightly-too-big slot of the crimp tool, mostly to keep the end from getting bent incorrectly in the correct-sized slot. Once this preliminary crimp was done, I used the correct-sized slot for the final crimp.
The crimped terminals should then slide right into the connectors and click into place. The red and brown wires are for power and ground and go into a two-terminal housing, while the orange control wire goes into a single-terminal housing. I found it hard to just push them in with my fingers, so I used pliers to hold the wire while I pushed down on the connector, which worked pretty well. Make sure it locks into place, or the terminal will be pushed out of the connector when you try to slide it onto the microcontroller’s pins.
These are the specific tools I used with links to Amazon, but you can get other brands if you prefer.
SN-28B Ratchet Wire Crimper Tool, which conveniently included a set of open barrel connectors and plastic housings with a 2.54mm pin pitch (the same as the ESP8266 uses). This model is good for 18-28 gauge wire; the micro servo wires are about 24 gauge.
SIQUIK 780 Piece 2.54mm Pitch Housing Connector Kit, in case you need more terminals and connectors.
Ideal Industries Stripmaster 20-30 Gauge Automatic Wire Strippers. Not strictly necessary, but it’s much easier to strip wires with these than pliers-like strippers.
Connecting The Servo to the Microcontroller
The NodeMCU ESP8266 has a nice feature where the Vin pin provides full 5v power from the USB port. This and the adjacent ground pin would therefore be able to fully drive the servo straight for the USB power supply, as long as it can source enough power. I discovered the USB hub connected to my iMac would only source 500 mA, which was enough to run the ESP8266, but would cause a reboot when trying to run the servo as it overpowered the USB port. For the purposes of programming and testing, I kludged extra power onto the Vin pins by adding a kind of pass-through board with a second USB port that plugged into the same hub. This worked perfectly fine. The final USB power supplies output 2A per port, which easily ran all the hardware of the NodeMCU’s USB port.
Mounting to the Register
After shortening the wires, I mounted the servo through the back of the printed mount with the spindle towards the bottom, securing it with the two screws it came with. I then connected the control wire to D5 on the ESP8266, and the power and ground wires to Vin and the adjacent Gnd. No other wiring is needed beyond the USB cable. The microcontroller was then secured to the mount with two self-tapping 5mm long M2.3 screws with the USB port pointing down. I got an assortment of screws from Amazon.
The arm screws right onto the servo spindle. These servos only have a 90-degree range of motion. which is greater than the range of motion of the damper, I still needed it to be positioned correctly to actually move the damper. I used pliers to manually rotate the servo and find the extents, then mounted the arm so that it would be able to fully open and close the damper. The exact angle isn’t important, as I will be calibrating the open and closed positions later in software.
For testing and calibration purposes, I zip-tied the servo and microcontroller assembly to the register. For both the zip tie mounting and final wall mounting I needed to drill a second hole in the center of the register, below the one that was already there. Final wall mounting is similar to zip tying, except that you mount the base to the wall first with two screws, then secure the assembly to the wall through the holes in the center of the base with two more screws.
The next step is to attach the damper hook to the back of the damper. After hooking the top of the damper hook part near the lever, the bottom snaps into place against the lower edge of the damper. It’s very tight, and some of the plastic definitely broke a bit, but it’s quite secure and isn’t going to come off. I also had to be sure that the top end didn’t wedge against the lever part or it would bind and keep the damper from moving smoothly.
The face plate and damper are then snapped back onto the register body. The trick here is to get the servo arm’s pin into the slot on the damper hook. This isn’t too bad on the bench, since you can reach through the bottom to get everything lined up. It’s trickier on the floor, where you have to first rotate the hook to the closed position, then position the damper so that the pin slips through the slot in the damper hook, and then you can snap the damper and face plate onto the register body. The pin is a little fragile, and if you twist it too much you’ll snap it off and have to print a new one.
Floor Installation
I replaced old, slightly rusty brown registers with these new white ones. Both registers are the exact same size, so I was able to pop out the old ones and install the new ones in their place. They’re held down to the wall with two sheetrock screws.
Before placing the servo and microcontroller assembly, I drilled a hole next to the duct outlet for the micro USB cable, being careful to avoid the screws that hold the duct to the joists. I then snaked the USB A end of a 10’ or 25’ cable down the hole and into the basement, running the micro USB end to the center of the duct and plugging it into the microcontroller.
The servo and microcontroller assembly sits in the middle of the duct, and screws into the wall through the two holes in the center of the register body. I again used sheetrock screws for this. Much of the assembly sits in the duct itself, and in some cases, the duct going through he floor was bent and would short against the microcontroller. I fixed this by applying some tap to the surface of the NodeMCU to insulate it. I made sure the USB cable was out of the way before fitting the damper.
Putting the face plate and damper on is trickier in the floor since you can’t reach under the register to line up the hook and the arm. As mentioned above, the best way I found was to rotate the servo arm all the way forward, carefully slip the damper hook onto the arm, and then snap the face onto the body.
USB Power Runs
All of the USB cables exited into the basement through the holes drilled into the floor. I mounted three USB power supplies at key points there. I thought I would have to install some outlets to run the power supplies, but luckily there were already some in good locations.
I set each power supply into a 3D printed “U” bracket and screwed it to a joist, then plugged it into an outlet and plugged in each register’s USB cable. I tacked the USB wires down with nail-in wire guides to keep things tidy. I used 10’ USB cables where possible, but sometimes had to resort to 25’ ones for longer runs, coiling up the slack. 15’ and 20’ cables would have been nice to have, but I didn’t want to go crazy buying cables for this.
On the second floor, the power cables ran from the side of the units into USB power supplies plugged into the outlet. I used some old Apple iPhone 5v 1A USB power supplies I had lying around for this.
On a per-device basis, 1A at 5v seems fine for both the small and large servos. I didn’t have any resets or any other issues from not having enough power.
Software
My goal was to use Apple HomeKit to control the dampers. The easiest way to do this is through a Homebridge server. While I’d prefer to run the HomeKit protocol directly on the EPS8266, this isn’t currently very easy to do, and I already had a Homebridge server running on my Mac mini.
To keep things simple I used the HTTP plug-in for Homebridge. Unfortunately, HomeKit doesn’t seem to have “heating register” as a device type, so I installed it as a “switch” type. While the software I wrote supports arbitrary damper positions, in practice I only need open and closed, so that’s all I implemented for HomeKit support. It was that or pretend it was a “light” with a “brightness” that controlled the damper position, and that was kind of silly.
Note that at the time of this writing, the package for the node.js HTTP plug-in was out of date relative to the GitHub version. After installing the package, I had to download the latest GitHub one and copy it over it. This is important if you want to use the real-time feature to get live status updates in the Home app, as there is a bug in the published version.
The Arduino source for the registers can be found on GitHub.
Prerequisites
My development environment is a Mac. I use the Arduino IDE to upload the sketch to the ESP8266. However, the board isn’t automatically found. I needed to install the Silicon Labs CP210x VPC driver in order to get it to work because the NodeMCU uses a CP2102 chip for USB communication. After that, it properly showed up as a port in the Arduino IDE — that is, as long as I used one of the USB A ports on my iMac (which is running macOS 10.15 Catalina). I had no success getting it to work with a USB C to A adaptor on either my iMac or my MacBook Pro. Googling suggests this is a common problem, but since it worked with USB A I didn’t spend too much time on it.
You also need to install the ESP8266 board definition for the Arduino IDE. Instructions for this can be found here. When choosing a board from the IDE, it’s “Generic ESP8266 Module”.
Dependencies
The Arduino sketch requires a few common libraries to work. These are installed via the Arduino IDE Library Manager.
EEPROM. This works a bit differently on the ESP8266, as it doesn’t have an EEPROM. Instead, you reserve part of the flash memory as storage for the EEPROM library, and all values are committed to flash at the same time. Also, you can only read/write single bytes with the EEPROM functions, which is mostly fine for my purposes. The values in the “EEPROM” are initialized to 255 by default, which can be used to tell if a setting has been stored yet.
Servo. Standard library to easily control any number of servos. One caveat I found is that if I remain attached to the servo after moving it, I’ll get buzzing as the servo tries to hold its position. This is annoying, so I attach to the servo, move it to its new position, and then detach from it. The servo is strong enough to hold its final position without power. This both reduces power usage and avoids the buzzing noise.
ESP8266, which provides the ESP8266Wifi, ESSP8266WebServer, and ESB8266mDNS includes, and which needs to be added to your library path as described in Prerequisites above. This is used to actually connect to your wifi network with the ESP8266, host the web server for Homebridge communication and web-based configuration, and provide mDNS support. With mDNS you can access the ESP8266 with “hostname.local” instead of by IP address.
ArduinoOTA. This allows for over-the-air firmware updates via wifi, which makes it a lot easier to update each of the devices if I need to change something. I had a lot of trouble getting the hostname set properly for mDNS before, but finally got it to work with ArduinoOTA’s setHostname() method. Not sure why that worked, but it does.
Be sure to properly configure the flash size in the IDE before sending the sketch to the NodeMCU. In my case, it was 4 MB. The SPIFFS are used for a pseudo-file-system, which I don’t use here (but probably should have for the configuration page), so the number of SPIFFS doesn’t matter here. If you don’t set this high enough, you won’t be able to use ArduinoOTA to update the firmware over wifi, and if you set it later you need to be aware that it will erase or corrupt anything already in flash and will have to re-enter that data again.
Config Storage and Options
I have two kinds of configuration state:
The compile-time configuration includes the wifi access point name and password. This could have been done through the serial terminal, but it was not going to change between register instances, so I just hard-coded it. I did put it in another file, though, so that the config settings were all together. A default base hostname is also stored here. Of course, a few months later I changed the SSID and needed to pull each NodeMCU and plug them into my Mac to change it, but that also gave me a chance to update other parts of the firmware and switch to larger servos.
Per-device settings are managed through the EEPROM library, which writes to part of the EPS8266’s flash memory. Here I store the open and closed position of both servos (the software still supports two, even though I only use one now) as 0-255 values, the open/close speed as a 0-255 value, the hostname as a 32-byte character string, and a boolean indicating if LED should be on or off. I documented the memory layout in the main file, using defines for the memory offsets to reduce the reliance on magic numbers in the code.
Calibration and Options
The servos I’m using are simple, and just move to where they are told. They don’t report their positions, and I didn’t install limit switches at the extent of their range. This kept costs down and builds simple, but requires a one-time configuration to set the opened and closed positions. I could have hard-coded it, but then I would have had to get the angle of the arm exactly right when mounting it on the spindle. Doing a separate calibration step is more forgiving than and easier to tweak. About half of the servos were programmed with a simple serial interface that took single-character commands to change the servo position, store that position as the opened or closed preset, and let you run it to those positions.
The remainder were configured with an HTML-based configuration interface that I added soon after. This works without the serial monitor, allowing servo calibration and the hostname to be set after installation if desired, while also providing access to some testing tools and other options. For example:
Host Name: Set a custom hostname. The default hostname is generated from a base string (“heatreg_”) and the MAC address.
Servo Positions: Provides an interface for setting the open, closed and current positions. Generally you'd set the current position to find an ideal value for the open and closed position, then enter that into the appropriate field for permanent storage. This is usually done on my desk, not with the registers in the floor, but if I break an arm it can be useful to redo it from the installed location.
Servo Speed: Normally, setting the servo position moves the arm as quickly as possible. This works, but the damper would tend to slam into its new position, which was a bit noisy due to the register’s sheet metal construction. After installing a few registers I added a servo speed setting. Changing the speed requires code, as you have to change the servo position in increments over multiple time steps until you are at the final position. Also, the interpolated position change runs from the last moved position to the desired position. If the damper is moved by hand or otherwise not in the last known position, the result will be a jump to what it thinks was the last known position before it starts to slowly move to the new position. This is a side effect of not knowing where the servo is at all times, and really isn’t worth the effort for me to implement. Most of the time it is electrically controlled to the full open or closed position, and thus the last position is known and this isn’t a problem.
LED Toggle: I have the blue ESP8266 LED on by default, but it’s pretty bright. I wasn’t able to get it to dim with PWM (I feel like it should work, but it doesn’t for some reason), but I did add an option to toggle it on or off.
Open and Close Buttons: These just move the servo to the stored open and closed positions. The “fast” versions do this as quickly as possible, while the default button uses the servo speed setting.
Blink LED: In case I forget which register is which, this lets me blink the LED to find it visually.
Reboot: Mostly for when changing the hostname. I could have disconnected and reconnected to the wifi, but this was simpler.
I elected to use a form written in pure HTML with a little CSS for the configuration page, and skip fancier Javascript scripting and live updates and the like. The raw HTML is stored as a C string into the code, with printf formatters for substitutions. This does use at least 4K of RAM for the template page, and another 5K for the generated page. I had a few crashes when I made the page bigger and overflowed by generation buffer, but that was simple enough to fix. I’m currently using under 70% of program space and under 50% of dynamic memory, so I’m not risking running out of RAM here. I could (and probably should) have stored the page in flash or at least removed the spaces and formatting in the HTML to save memory, but for a first project with a web server hosting a page I elected to keep it in the source code.
Firmware Updates
I figured I’d need to update the firmware occasionally. There are three options:
Uninstall the microcontroller and servo assembly, bring it to my computer, and update it there. This requires only removing the damper and face plate and the two screws that secure the assembly to the wall, but it is a bit of a pin.
Bring a laptop into the basement and plug it into the USB cable. This would have worked if I could get around the USB C issues I was having with the Silicon Labs driver on my MacBook Pro, and also if I had bought true (and more expensive) USB cables with data instead of just USB charging cables.
Use ArduinoOTA to do over-the-air updates over wifi. An important detail is to set the flash memory size in the IDE to the size of your board (say, 4 MB for the NodeMCUs that I was using) as above, or you’ll get a “not enough memory” error. Of course, I didn’t realize this was necessary before I re-flashed everything, so I’ll have to do a manual flash again before I can do an OTA flash. Note that changing the flash size in the IDE will erase anything already stored in flash. I did a test on one of the as-yet-uninstalled registers, and it worked perfectly.
So far I’ve only had to do one update to all the devices to load the software with ArduinoOTA support and the web interface, and I did that by removing each device and bringing it to my computer. I haven’t had to do another update since then, but I’ll be trying ArduinoOTA for that.
Homebridge Support
Homebridge is a node.js application that runs on a computer, in my case a Mac mini. I had set this up some time ago for Nest and SmartThings access in HomeKit. For the registers, I’d originally used the HTTP plug-in before switching to the HTTP-Webhooks plug-in.
The HTTP Plug-in
This plug-in seems to be the most common one, but it has some issues that faced me to switch away. I’d recommend just starting with HTTP-Webhooks and skipping this entirely, but I’m documenting it anyway to illustrate the pitfalls and describe how it might be set up.
Installing the plug-in is as per the instructions on the site, via the node.js package manager. However, the version in npm is not up to date relative to the one in GitHub. Most significantly, there is a bug that causes the app to crash if you try to get real-time status updates when the device isn’t currently on the network. To work around this, you need to install the latest version from GitHub I simply downloaded it as a zip and copied it over the version downloaded by npm.
Here’s an example of the HTTP plug-in entries for the first two heating registers.
"accessories" : [
{
"accessory": "Http",
"switchHandling": "realtime",
"name": "Heating Register 1",
"model": "HeatReg01",
"on_url": "http://192.168.1.220/on",
"off_url": "http://192.168.1.220/off",
"status_url": "http://192.168.1.220/status",
"http_method": "GET",
"service": "Switch"
},
{
"accessory": "Http",
"switchHandling": "realtime",
"name": "Heating Register 2",
"model": "HeatReg02",
"on_url": "http://192.168.1.221/on",
"off_url": "http://192.168.1.221/off",
"status_url": "http://192.168.1.221/status",
"http_method": "GET",
"service": "Switch"
}
]
Note the comma after the curly brace when adding additional accessories, but no comma on the final closing braces. Ah, the fun of JSON. Don’t forget the closing square brace at the end of the “accessories” array.
I had trouble getting mDNS to work initially, so I used DHCP reservations on my router for the MAC addresses of each register, thus allowing access through a static IP. Here’s a breakdown of the fields:
accessory: “Http” to tell Homebridge that the rest of the data pertains to the HTTP plug-in.
switchhandling: When set to “realtime”, the “status_url” is polled about once a second. This gives the best feedback in the Home app.
name: The name of this accessory, which will be displayed in the Home app.
model: The model of the accessory. Not really that important, and for your own use.
on_url: This is called to turn the device “on”, which in this case means to open the register damper.
off_url: Turn the device “off”, closing the damper.
status_url: Returns 1 if the device is “on” , and 0 for “off” as plain text
http_method: How the on, off, and status URLs are called. I just kept it simple and used GET, which is easy to test with a web browser. POST is more correct, though.
service: The kind of device this is. There isn’t a “heating register” device, so I just registered it as a switch. Note the capital ‘S’; this is important, or the device won’t be recognized. You could use “Light” and then use the “brightness” URL to set a position between open and closed, but generally, that isn’t really necessary, so I didn’t bother. Also, it avoids all the vents closing when Siri mistakenly hears me say “turn off the lights”.
The ESP8266 implements all of the URLs above, along with some other useful ones for configuration and testing purposes. The status_url explicitly tests for if the servo was last moved to the open position, and return “on” for that and “off” for all other states.
After running Homebridge with the registers powered up, you can control them directly from the Home app in iOS, iPadOS or macOS. I created a new “room” in the Home app called Forced Air and put all the registers in that, mostly to avoid accidentally triggering them with Siri commands addressed at an existing room. I then created some scenes to close or open all the registers in a particular room, such as Open Dining Vents and Close Den Vents.. I also rigged up some vents to the Goodnight scene to close much of the first floor when we were going to sleep. In the morning, an automatic Predawn scene would turn on some dim lights on the first floor and open up those vents again. The Let’s Eat scene would open the dining room and kitchen vents (in addition to turning on the lights and waking the Apple TV there), while the TV Time scene would close those and the living room vents and open the den vents.
In the end, I set up all eight of the first-floor vents this way, plus the three second-floor vents. I was going to skip the second-floor ones, since it never gets too hot up there, but I had spares after accidentally installing the dampers on the return registers.
Separate Homebridge Instances for Robustness
For simple setups, you can put all your Homebridge devices in the same config and run them from the same instance. I now have homebridge-http for the heating registers, SmartThings, MyQ2 for my garage door openers, and Nest support set up. The problem is that if any one of these goes down, it takes down all the others.
For example, Chamberlain tends to change their MyQ garage door protocol somewhat frequently, and this would cause an earlier version of MyQ2 to fail completely, which would take down the entire bridge. By segregating them into their own bridges, you both ad robustness and improve performance by allowing HomeKit to talk to multiple servers at once. It also makes it easier to debug new devices without risking your entire bridged setup and associated automations and organization in HomeKit.
It’s fairly straightforward to run multiple instances of Homebridge. I separated each plugin out into its own instance, while also defining the homebridge-http instance as being specifically for the heating registers, as in the future I may add more instances supporting other home-brewed devices. Details on how to set up the instances can be found on the the homebridge-homeseer page on Gihub.
Each of my Homebridge instances is spawned by launchctl on my Mac. These are also set to redirect the output from stdout to a log file adjacent to that Homebridge instance’s config dir. A lot of good information on using launchctrl and launcd can be found at launchd.info. You set up an XML-baed plist file with the appropriate keys, such as having it run on system launch (RunAtLoad) and automatically relaunching it if it crashes (KeepAlive), load it with launcctl load pathToPList, and that’s it. If you want to stop the service, you replace load with unload; simply killing the task will just restart it if KeepAlive is set, so you need to explicitly unload it in that case.
Here’s the plist I use for the heating registers
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>Label</key> <string>com.homebridge.server.http-heatingreg</string> <key>ProgramArguments</key> <array> <string>/usr/local/bin/homebridge</string> <string>-I</string> <string>-U</string> <string>/Users/myUserName/homebridge/HTTP-HeatingReg</string> </array> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/local/bin/:$PATH</string> </dict> <key>StandardOutPath</key> <string>/Users/myUserName/homebridge/HTTP-HeatingReg.log</string></dict> </plist>
Kludging Around a Homebridge-http Connection Bug
Unfortunately, there seems that there is a a bug in Homebridge-http when you have a weak connection to a device, or the device can’t be reached at all. I run the HomeBridge server on my Mac mini, and about a day after changing my 2.4 GHz SSID but not updating the registers I found that my Mac could no longer make any HTTP connections at all from any app until I restarted homebridge. netstat reveled hundreds of connections to each register. There are “only” 16k sockets allowed for the IP range that includes HTTP. Everyone one of them appeared to be in use.
I believe that when the connection is intermittent or lost, homebridge-http tries to reconnect over and over again, but it doesn’t actually close the old socket first. This eventually uses up every socket available in that port range. I’ve opened an issue for it on Github.
I originally tried using cron/crontab for this, but I found it a bit too low level for my tastes. I switched to launchd, creating a new plist that runs a simple shell script three times a day (at 5 AM, 1 PM and 9 PM) to unload the launchd bridge instance, wait five seconds, and then reload it again:
#!/bin/bash # Stop homebridge-http launchctl unload ~/Library/LaunchAgents/com.homebridge.server.http-heatingreg.plist # Wait 5 seconds sleep 5 # Restart homebridge-http launchctl load ~/Library/LaunchAgents/com.homebridge.server.http-heatingreg.plist
The plist is as follows. Important details:
I had to add /bin/ to my path so the script could find it could find launchctl and i.
I did not include KeepAlive (since it’s a scheduled one-shot execution) or RunAtLaunch (it should only when scheduled).
I could have used cron-style formatting, but for three specific start times this was more readable and harder to screw up the syntax for.
It may or may not also be important to do the following. If you have problems, try these:
chmod 644 on the shell script. I tend to +x so I can execute it for testing as well.
Change the owner to of the script with chown root:wheel to avoid error 127 from launchd.
Check the console for your launchd task, which in this case is com.homebridge.server.http-heatingreg.periodicrestart, to see if there were any errors running your task. You may want to add RunAtLaunch for debugging purposes.
I also found Lingon X to be very useful. It’s quite affordable, and it provides a nice front end for editing launchd plists.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/bin/:/usr/local/bin/:$PATH</string> </dict> <key>Label</key> <string>com.homebridge.server.http-heatingreg.periodicrestart</string> <key>ProgramArguments</key> <array> <string>/Users/jangell/Documents/RestartHomebridgeHTTP.sh</string> </array> <key>RunAtLoad</key> <false/> <key>StartCalendarInterval</key> <array> <dict> <key>Hour</key> <integer>5</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Hour</key> <integer>13</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Hour</key> <integer>21</integer> <key>Minute</key> <integer>0</integer> </dict> </array> </dict> </plist>
Attempt at Rolling Logs
To keep the logs from getting unwieldy, I tried updating /etc/newsyslog.conf with this entry. newsyslog’s job is to rotate log files by periodically renaming them, and deleting old ones once a count has been reached. This is automatically run by launchd on macOS.
# Rename homebridge logs when they exceed 20 MB or at midnight every day, keeping up to 7 logs /Users/jangell/homebridge/*.log 644 7 20000 $D0 GJ
This breaks down as follows:
/Users/jangell/homebridge/*.log : Location of the files. It matches all files ending in .log, although I could have specified each file individually if I wanted to.
644 : Permissions applied to the renamed log files. This seems to be a good default, allowing anyone to read but only admins to delete them.
7 : How many logs to keep
20000 : Maximum size of a file in KB before a new log is created (here, 20 MB)
$D0 : An ISO 8601 restricted time format string indicating how often to create a new log, in this case every night at midnight.
GJ : Flags indicating that a wildcard pattern is in the filename (G") and that the files should be compressed (J).
Details on all of this can be found on the man page. I found this post helpful as well, although he is setting up a new instance of newsyslog that will run with a separate instance of launchd, while I just had mine use the existing instance.
Be sure to run sudo newsyslog -nvv to make sure you didn’t make any mistakes. If successful, it will display what it will do next, such as this:
/Users/jangell/homebridge/HTTP-HeatingReg.log <7J>: --> will trim at Sat Aug 15 00:00:00 2020
Unfortunately, this didn’t really work. Since the Homebridge instance runs continuously, it gets confused when newsyslog renames the go file and creates a new one, at which point Homebridge just hangs until you delete the log. For now, I’m just keeping an eye on the logs manually until I come up with a better solution.
Switching to HTTP-Webhooks
HTTP-Webhooks is newer and more robust than the HTTP plug-in. It supports many more device types, and provides a way for devices to push state changes to homebridge (that’s the “webhooks” bit). As best I can tell, webhooks just means that you have two-way communication by, in this case, having the device call a URL to push state to homebridge in addition to homebridge being able to make its own requests by calling a URL on the device.
The change to the Arduino code was simple: on a change to the heating register state, the device calls a URL at the IP of the Mac mini running Homebridge with the “webhook_port” defined in the config.json for HTTP-Webhooks with the accessoryId and the new state (true for open, or false for closed). The existing on/off URLs I set up on the device for the old HTTP plug-in works without modification for the HTTP-Webhooks implementation.
Setting up HTTP-Webhooks is very similar to HTTP. It is again installed through rpm, and I again configured it to run on its own node.js instance. I kept the config simple, mostly just setting a unique username and port for the Homebridge instance.
Instead of adding a series of accessories, you add a platform that contains the accessories, organizing them by their device type. My registers show up as switches, so I had a “switches” block with each in turn. The “id” uniquely identifies a register to HTTP-Webhooks, and is how the heating register itself pushes updates to Homebridge.
An important detail when setting up HTTP-WebHooks is that you must set the “cahce_directory” to a real path when running it from launchd. By default, it uses your user directory, but launchf doesn't have a user directory, so the cache directory won’t be found and Homebridge will fail to start up.
Here’s a very basic example of the platforms section of the config. It sets the cachedirectory to a real path, and sets the webhookport to an unused port. It then defines two switches that were set up with static IPs (note that the Mac mini also has a static IP so that the heating registers can find it easily). The “id” is set to the hostname configured for each register, while the name is disabled as the default in the Home app. Note that changing either will cause the device to be considered “new” in Home and will need to be re-added to any rooms and automations you may have set up. The “on” and “off” URLs are just like in the HTTP plug-in.
"platforms": [ { "platform": "HttpWebHooks", "cache_directory": "/Users/myUserName/homebridge/HTTPWebhooks-HeatingReg/cache", "webhook_port": "51835", "switches": [ { "id": "heatReg2", "name": "Studio Register 2", "on_url": "http://192.168.1.221/on", "off_url": "http://192.168.1.221/off" }, { "id": "heatReg3", "name": "Kitchen Register 3", "on_url": "http://192.168.1.222/on", "off_url": "http://192.168.1.222/off" } ] } ]
How Many Vents to Close
AAAs a final note, you should never close all the vents in the house, or even the majority of them. Doing so could build up pressure in the system and damage the blower or the ducting, as there is no place for the air to go. I leave some of the vents open in the living room much of the time, even though we don’t use that room very much, mostly because that’s the room the thermostat is actually in, and that determines when the house is up to temperature. Luckily, the vents aren’t air tight and leak a bit anyway, so
Maintenance
There really isn’t much maintenance needed not here. There just isn’t a lot that can fail.
I did have to replace one of the servos. I think it was just bad, and would vibrate when in the open position, even though the control wire was disconnected in software. I replaced it with a new servo and it was fine.
A second register stopped working properly, which was due to the power terminal sliding slightly out of the connector on the NodeMCU. I replaced the terminal, made sure it locked in place, and it worked fine after that.
Overall, everything has been working well, and the temperatures have been noticeably better in the rooms we’re actually in, so I’ll call this a success.