Press "Enter" to skip to content

nuxx.net Posts

HDMI-CEC to Onkyo RI Bridge

ESPHome device, a Seeed Studio XIAO ESP32S3 and level shifter with 3.5mm TS and HDMI connectors.

After getting the Onkyo RI support for ESPHome and Home Assistant in place, it was neat that I could turn my Onkyo A-9050 amplifier on and off remotely, but it wasn’t actually very useful; it didn’t save me any time/hassle. This iteration, adding HDMI-CEC support, brings it all together.

Back when I started this project, my main goal was to find a nice way to deal with toggling the power on the amplifier. Because I only use a single input on the amplifier and volume is already handled by the Apple TV remote, I don’t use the remote and it’s stored away in the basement. Normal practice was to manually press the power button on the front before using it, but this was irritating so I went looking for a better way, and the result was this project.

Initially I was looking at a way to use Home Assistant to coordinate powering the Apple TV and amplifier on, but it turns out there’s no good way to power up an Apple TV remotely; or at least not from anything that’s not an Apple device. I thought about going down the path of figuring out how the iOS / iPadOS does it, but the results of that would need to be incorporated into pyatv and chasing Apple’s changes was not a path I wanted to go down.

I then began thinking about it inversely: What if I could tell when the Apple TV woke and slept, and then take action based on that? After all, it’s already using the well-established Consumer Electronics Control (HDMI-CEC) to wake the TV… What if I could listen for that? And we’re always using the Apple TV remote when watching content and there’s no need to wake it while out of the room, so pressing a button on the remote to get things started is just fine.

Well, it turns out that was easier than I thought. Using Palakis/esphome-native-hdmi-cec, a HDMI-CEC component for ESPHome, and then doing a little protocol analysis I now have a device that:

  • Listens for the Apple TV to wake up and sends and sends a Power On to the receiver.
  • Listens for the Apple TV to go into standby and sends a Power Off to the receiver.
  • Sends events to Home Assistant whenever a broadcasted HDMI-CEC Standby (0x36) or Report Power Status (0x90) are received.
  • Exposes controls in Home Assistant for a variety of Onkyo remote control commands and broadcasting an HDMI-CEC Standby (0x36). The latter puts my TV and the Apple TV to sleep, and also gets heard by ESPHome (loopback) and results in the amplifier being powered off.
  • Exposes a service in Home Assistant allowing arbitrary HDMI-CEC commands to be sent.

The result is that when I press a button on the Apple TV remote to wake it up the amplifier powers on, the TV wakes up (as before), and all is ready to go with one button press. This satisfies my original goal, and also allows some lights to be turned on automatically.

I’ve still got some lingering architectural questions and may be digging further into the HDMI-CEC stuff to see if I can make it work better, but for now I’m happy. If/when I take this further, the big questions to answer are:

  • Currently ESPHome powers on the amplifier without Home Assistant. This feels rational for a device bridging the two protocols and makes the amplifier work more like a modern HDMI soundbar, but is it the best way to go? Running it all through HA would be a lot more complicated and network (and HA) dependent, but I could instead use the notification in HA trigger a Power On at the receiver. Are there ever situations where I’d want this device to not power on the amplifier?
  • The HDMI-CEC implementation is very simple, solely listening for two messages I saw the Apple TV send and taking action on them. One of these, Report Power Status, is per-spec used to send more than notifications of power being on. Should this be changed or further built out? (Note: Because the library doesn’t implement DDC for device discovery and addressing and such, it can’t be a full-fledged implementation. But that much is likely not needed; there’s more I can do.)
  • Is it possible to wake the Apple TV via HDMI-CEC? It’s not immediately obvious how, but perhaps with a bit of probing…?

Hardware-wise, this was simple to do. All it required was getting an HDMI connector (I used this one), connecting pin 13 (CEC) to a GPIO, pin 17 to ground, and pin 18 to 5v (VUSB) as per the readme at Palakis/esphome-native-hdmi-cec. Since CEC uses 3.3v there was no need for a level shifter as with Onkyo RI. I was able to add this on to the previous adapter without a problem and everything just worked.

With this ESPHome configuration I changed things around a bit, both to simplify and secure the device and make things better overall. As I learned more about ESPHome and started thinking about securing IoT devices, I wanted to minimize the ability to do OTA updates, including via the web UI, and access the API. I also wanted to pull credentials out of my .yaml file so I could more easily share it. Changes to support this, and some other nifty things, are:

  • Setting up a secrets.yaml to hold wifi_ssid, wifi_password, ota_password, and api_encryption_key.
    • Tip: All this involves is creating a secrets.yaml file in the same directory as the configuration .yaml and putting lines such as wifi_ssid: "IoT" or api_encryption_key: "YWwyaUNpc29vdGg3ZG9oazdvaGo2YWhtZWlOZ2llNGk=" in it. Then in the main .yaml reference this with ssid: !secret wifi_ssid or key: !secret api_encryption_key or so.
    • Generating an API key can easily be done with something like: echo -n `pwgen -n 32 1` | openssl base64
  • Setting a password for OTA updates.
    • Note: Once this password is set, changing it can be a bit complicated (see ESPHome OTA Updates for more information). I suggest picking one password from the get-go and sticking with that.
  • To further minimize unapproved access, I did not enable the fallback access point mode, the captive portal, and disabled the web server component (because it’s unauthenticated and allows firmware uploads). I’m still thinking about disabling safe mode.
  • Set name_add_mac_suffix: true to add the MAC address suffix to the device name. This makes it easier to use one config on multiple devices on the same network, such as when doing development work with multiple boards. (See Adding the MAC address as a suffix to the device name.)
  • Because my Onkyo RI PR has not been merged (as of 2024-Sep-01), I had been manually patching to add it. It turns out that some PRs can automatically be incorporated into the config via external_components, and this works great for my needs until this gets merged:
external_components:
  # Add the HDMI-CEC stuff for ESPHome
  - source: github://Palakis/esphome-hdmi-cec
  # Add PR7117, which is my changes to add Onkyo RI. Had not been merged as of 2024-Sep-01.
  - source: github://pr#7117
    components:
      - remote_base

Despite stripping the configuration back a bit to secure it better, which in turn removes on-device overhead, I still have problems with the OTA update on the Seeed Studio XIAO ESP32S3. This is irritating because it means any changes require connecting a cable to flash it via USB, but I can also keep using the breadboarded SparkFun ESP32 Thing Plus for any future development.

The configuration I’m using can be found here: hdmi-cec-onkyo-ri-bridge_2024-sep-02.yaml

Note that this includes some development HDMI-CEC buttons, such as sending EF:90:00 and EF:90:01. This is part of some experimenting in attempts to wake up the Apple TV via CEC, but thus far doesn’t do anything. However, they serve as good examples of how to send multiple bytes to the bus. It also includes commented sections for the different ESP32 boards I’ve used and will likely need to be changed for your purposes.

Comments closed

Onkyo RI for ESPHome / Home Assistant

ESPHome Devices for Onkyo RI output; final and prototype.

Our living room has a very simple setup: a non-networked TV, an Apple TV, and an older Onkyo A-9050 amplifier that drives two small speakers and a subwoofer. It’s a great sounding yet simple setup for two channel audio, perfect for the basic streaming video watching we do.

Being older the amplifier doesn’t have any of the modern (eg: HDMI CEC) mechanisms for controlling it, but it does have a 3.5mm tip sleeve input on the back for Onkyo RI. This old, proprietary system uses a wired connection creating a bus that allows different Onkyo components to be controlled from one central component and thus one IR remote control.

This protocol is well documented, both via the LIRC project and some other sites (ref: LIRC documentation, Onkyo RI Protocol, docbender/Onkyo-RI) so this got me thinking it’d be pretty easy to implement in ESPHome and thus make the receiver controllable from Home Assistant. While this is only one-way control (since it’s basically a wired version of an IR remote), it would still allow for remote power on/off, input changing, etc.

After a few false starts, it turns out it was easy. Thanks to some pointers from folks in the ESPHome Discord I realized the best way was adding support for the protocol to the existing Remote Transmitter integration. Since this integration already had other protocols which used similarly timed protocols it was pretty easy for me to add Onkyo RI by copying the structure from another and modifying it for this protocol. (For reference, it’s not standard serial and requires specific timings, so it wasn’t as simple as just using a UART.)

I’ve since submitted PR #7117 to the ESPHome project to contribute this back, but despite passing all tests I’m still waiting for it to be accepted. (I looked into creating a custom component that could be included from another GitHub repo, but since this was best implemented by modifying an existing component, that didn’t make sense to me.) Until this gets accepted, I’ll just have to build esphome locally or if others want to do it, patching things based on the files in the PR.

Getting it all wired up was pretty simple with the only thing needed was getting electrical levels right, as the ESP32 microcontrollers use 3.3V logic and RI uses 5V. Thankfully a simple level shifter based around a FET can handle this. I first prototyped it with a SparkFun ESP32 Thing Plus and an Adafruit BSS138 on a breadboard and this worked great.

I really don’t like the idea of having a fragile and ugly breadboard sitting in the living room so I made plans to replace it with something smaller. After ordering parts and letting them sit for a few weeks, I finally got around to it one rainy Sunday afternoon.

This smaller, final implementation uses a Seeed Studio XIAO ESP32S3, a cheap level shifter board from Amazon (electrically identical to the Adafruit BSS138), and a 3.5mm TS cable. This was all wired up then bundled, along with the ESP32’s external 2.4 GHz antenna, into the a single blob inside of some heatshrink tubing making for a simple, streamlined final package. This works great, and now I have a single thumb-sized module with a USB-C connector (for power input and reprogramming) on one end and a 3.5mm plug on the other for the receiver. And it shows up wonderfully in HA and works as a remote control.

While the initial prototyping went great, I did run into two problems worth mentioning:

First, the one of the super-cheap level shifters I got from Amazon seemed to be bad. After hooking it up levels seemed all wrong, and I was seeing 3.3V at the ESP32 end and a solid 5V at the plug. Turned out to be a bad level shifter (or perhaps bad PCB) but by moving to the second shifter on the same board things were fine.

Second, when attempting to do an OTA update after the Onkyo RI firmware on the ESP32 S3 is running, it fails, indicating that Component esphome.ota took a long time for an operation (7339 ms)..

If I flash it back via USB with a default ESPHome config (via ESPHome web), it then OTA updates fine. This only happens on the ESP32 S3 and didn’t happen on the ESP32 WROOM, and seems related to how long the OTA takes on this module or maybe something caused by wireless transmission speeds? I didn’t try a serial upload nor troubleshoot any further as I both have a good workaround and see no need to reprogram the device any time soon.

The ESPHome configuration used for the final version can be found here: onkyo-a-9050_seeed_xiao_esp32c3.yaml. This uses a handful of commands that I tested to work on the A-9050. For other Onyko RI receivers there may be different commands needed; I suggest consulting the protocol docs mentioned above to discover others. I made a point of adding rational icons to each so that once added to Home Assistant things look good.

Using these is then nice and straightforward in HA, such as a basic button here on my dashboard which sends the Toggle On/Off (power) command:

type: button
show_name: true
show_icon: true
tap_action:
  action: toggle
entity: button.onkyo_a_9050_toggle_on_off
name: Onkyo A-9050
hold_action:
  action: none
icon_height: 40px

I’m not sure where I’ll go next with this. Toggling power on the receiver from a dashboard is neat, but not that important. Ideally I’d like to have a single automation that will change a couple of lights, turn on the receiver, and result in the Apple TV and television itself being turned on, but there’s still pieces missing to allow this.

It seems an Apple TV can’t be woken over the network when sleeping, the TV is not network accessible, and the receiver does not transmit status. So, I can’t do this with my current setup. I believe that it may be possible to build an ESPHome HDMI CEC device and connect it to another input on the TV to wake things up using something like Palakis/esphome-native-hdmi-cec, but that’ll be another project… At least now I’ve got a spare breadboarded ESP32 to start down that path. Time to order some HDMI breakout connectors, I guess.

Comments closed

New XC Bike: Pivot Mach 4 SL v3

I never really thought I’d have a fast XC bike again, but, I was wrong. And I’m glad. And impressed at just how capable a “modern” XC race bike is.

For almost three years I’ve had the amazing Pivot Trail 429 (v3) as my main mountain bike. It’s an incredibly capable trail bike that I’ve ridden all over the midwest, from Arkansas to Copper Harbor to Tennessee, and it’s been wonderful. But, being a longer, slacker bike the one place I wasn’t super-keen on it was our standard Lower Peninsula classic XC trails, especially those with fast, tight, twisty sections. It’s fun, but just not ideal for that stuff and felt like I had to dump the front end into the really tight corners.

In late 2022 I unexpectedly found myself buying a used-but-like-new super-well-equipped Pivot LES hardtail from a buddy (price I couldn’t pass up) which once again gave me a taste of a proper XC bike. While I used it for lots of local rides, and both Ore to Shore, and Lumberjack 100 in 2023, it never quite clicked. I had an absolute blast riding it, especially on the smooth sandy trails in the northern LP, but maybe due to the lack of dropper or the 100mm fork or my weird body proportions, it was fun and fast but never quite felt like mine. I liked having it around, and it was fun, but I was routinely eyeing other XC bikes.

When Pivot announced the v3 of the Mach 4 SL in 2023 it looked really appealing, but I couldn’t justify it. I put thoughts of a new bike aside… Until now. After a lot of thinking and basically coinciding with my birthday, a brand new Pivot Mach 4 SL v3, Pro XT/XTR build with carbon wheels, in the eye-catching Seafoam Green, found it’s way home. (And away went the LES, passing on the good deal to a buddy who was looking for a superlight XC race bike.)

Unlike normal for me, the build is almost completely stock. It’s incredibly well equipped as shipped, so outside of contact points (saddle, grips) the only changes I made were for fit, swapping in a shorter crank and a lower-rise bar.

The bike shipped with a really nice Race Face Aeffect crankset and a 34t ring, but had 175mm arms. I’ve been riding 170mm cranks on the Trail 429 for years, rather liking it, and wanted the same on here. Pleasantly surprised to see the frame fitted with a Shimano bottom bracket, I was able to swap in an XT crankset with no extra work. After confirming with Pivot that the bike is spec’d with a 53mm chainline crank I ended up going with the 52mm FC-M8100-1 which Shimano spec’s for both Boost (148mm) and standard (142mm) rear ends. The other option was the Boost-only 55mm FC-M8120-1, but sticking with the narrower chainline improves big-cog performance, so it’s preferable if possible. The narrower FC-M8100-1 fits the Mach 4 SL v3 perfectly, with plenty of clearance around the ring and arms, works wonderfully, and was a great choice.

Shimano XT FC-M8100 crankset with 34t ring fits very nicely with plenty of clearance.

Chainring-wise I wanted to stay with a 34t, but being (weirdly) cheap I opted for the SLX-level SM-CRM75. It’s only ~10 grams more than the XT-level SM-CRM85 but can be found for around half the price ($33 vs. $59). Both have steel teeth and an aluminum carrier, and upgrading to the XT only gets an anodized spider (vs. painted) and carbon fiber (vs. glass fiber) non-load-bearing plastic bits. Thus, performance is effectively identical. Due to series colors the center spider is a little bit of a blueish grey and doesn’t match the other parts, but in the overall scheme of the bike it looks fine, and even the smallest amount of dust obscures the variance.

Where I didn’t go cheap was on the pedals… My go-to pedals are typically XT-level PD-M8100 (regular) or PD-M8120 (trail), but finding XTR PD-M9100 on sale at the price XTs would normally sell for I figured I’d give them a go. They are only marginally lighter (28g/pair), but have a smaller center that should clear dirt and debris better, and a slightly shorter stack height. But mostly, I liked how they looked, wanted to see how they felt, and wanted new pedals for a new bike. Thus far they seem good, and I don’t regret purchasing them.

Grip-wise I put aside the stock Pivot Phoenix Factory Lock-On Grips and fitted ESI Extra Chunky grips in black with generic BMX-ish screw-in bar plugs. The narrow bar plugs are only slightly larger than the bar diameter, meaning they don’t press on my hand when my hand sits at the outer edge of the bar. While the Pivot grips are excellent I prefer foam ESIs on my mountain bikes. Kristen really likes the Pivot grips, so these’ll end up set aside for her.

The stock saddle is a fairly narrow Pivot-branded WTB which just doesn’t work for me, so the 143mm-wide Specialized Power saddle (conveniently with titanium rails) from the LES is now on the Mach 4 SL.

The bike also originally shipped with a 760mm wide, 20mm rise, 5° upsweep, and 9° backsweep bar. I swapped this out for another Pivot-branded bar I’d bought from a friend earlier in the year. After cutting 5mm off of each end it’s now 750mm wide, 6mm of rise, no upsweep, and 9° of backsweep. This is in line with what I have on my trail bike and so far feels good, although I may go down to 740mm or maybe even 730mm after I ride it more.

Fitted with everything including a bell (but no bike computer), the bike is a smidge over 26 pounds. While I wasn’t focused on weight with this build, I did pay attention as this level of bike implies it’ll be fairly light, and this impresses me. While it’s obstensibly an XC bike, and my previous (the LES) was under 21 pounds, this is far more capable: 120mm / 105mm of travel, dw-link suspension, 51t cassette, dropper, etc. Not long ago this would have been called a trail bike, and I think I’d ride it on anything that I personally am willing to do.

My first ride was at Stony Creek and thus far I’m incredibly happy. The redeveloped trails here have everything from lumpy rocks to smooth trails, small drops and wood features to semi-steep rock rolls, perfect for a bike shakedown and demo. The Mach 4 SL felt amazingly fast and comfortable on everything without really trying; just simply composed. It handles the tight/twisty stuff the way I was looking for, yet still feels really good on technical features and small drops. It’s clearly not as burly and squishy as the Trail 429, but that’s the point.

Pedaling firms up the rear end while still absorbing little bumps and keeping the tire in contact with the ground. Rolling over rough spots or dropping off things just feels right. The factory tire choice of a Rekon in the front and Rekon Race in the rear is a good one, as it both rolls nicely yet still has plenty of traction to handle aggressive steering. I also think a 34t ring coupled with the 10-51 cassette will be right. (On the LES I had a 34t, but with the 46t cassette it was a bit too tall for longer climbs. This should be much better.)

I have a bit more suspension setup to do, as in the factory-recommended settings the fork’s damper is a bit noisy and feeling not-quite-right, but dialing that in will just take a bit of time. I may toss the ShockWiz on there as I typically do to try and dial things in further. But, one thing at a time.

Initial build details are as follows:

Frame: Pivot Mach 4 SL v3 (Large, Seafoam Green)
Fork: Fox 34 Factory Step-Cast GRIP SL (2025, 34, K, FLOAT SC, 29in, F-S SC, 120, Grip SL, 3Pos-Adj, Matte Blk, No Logo, Kabolt 110, BLK, 1.5 T, 44mm Rake, N/M OE – Part: 910-31-870)
Fork Axle: Fox Kabolt
Rear Shock: Fox Float Factory (2025_24, FLOAT, F-S, K, 2pos-Adj, Evol LV, Pivot, Mach 4 SL MD-XL, 190, 45, 0.5 Spacer, CM, RM, CMM, No Logo, OE – Part: 972-05-949)
Headset: Pivot Integrated Race Headset
Crankset: Shimano XT FC-M8100-1
Crank Arm Protectors: PRO Crank Protector (PRAC0144)
Bottom Bracket: Shimano BB-MT800-PA
Chainring: Shimano SLX SM-CRM75 (34t)
Chain: Shimano CN-M8100
Derailleur: Shimano XTR RD-M9100-SGS
Shifter: Shimano XT SL-M8100-IR
Shift Cables / Housing: Jagwire / Shimano OE
Cassette: Shimano XT CS-M8100-12 (10-51)
Brakes Calipers: Shimano XT BR-M8100
Brake Levers: Shimano XT BL-M8100
Brake Pads: Shimano J04C-MF (Metal w/ Fin)
Brake Rotors: Shimano RT-MT800-S (160mm)
Stem: Pivot OE Aluminum (60mm x -6°)
Bar: Pivot OE Carbon (Diameter 35mm, Width 760mm, Rise 6mm, Sweep 9° – Cut to 750mm)
Wheels: Reynolds Blacklabel 309/289 XC
Tires: Front: Maxxis Rekon 29 x 2.4WT 3C/EXO/TR (TB00017500), Rear: Maxxis Rekon Race 29 x 2.40WT EXO/TR (TB00211100)
Seatpost: Fox Transfer (31.6mm x 150mm, Original Version)
Dropper Lever: Fox Transfer Post Lever (I-SPEC EV Mount)
Seatpost Collar: Pivot OE
Saddle: Specialized Power Expert (143mm)
Pedals: Shimano XTR PD-M9100
Grips: ESI Extra Chunky (Black)
Bar End Plugs: Generic Aluminum BMX-Type (Black)
Bottle Cages:Specialized Zee Cage II (Black Gloss, 1x Left)
Computer: Garmin Edge 840
Speed/Cadence Sensors: Garmin Bike Speed and Cadence Sensor
Computer Mount: Best Tek Garmin Stem Mount, Stem Mount for Garmin Computer, Adjustable Black
Bell: ROCKBROS Bike Classic Bicycle Bell (Black)
Derailleur Hanger: SRAM Universal Derailleur Hanger
Frame Protection Tape: McMaster-Carr UHMW PE

Comments closed

Fork-Mount Bike Rack for Honda Odyssey (2018+)

Recently I purchased a Honda Odyssey (2024 EX-L) to replace my aging Subaru Outback and get something a bit better for longer trips. Specifically, I wanted to be able to easily put two (or more) bikes inside, along with dogs and luggage. During winter we’d often drive to trails separately because that was the easiest way to keep both of our bikes clean on trips and this was irritating.

To securely hold the bikes inside I put together an adjustable, expandable rail system that holds bikes by their front through axles and is easy to adjust based on bike type, carrying needs, etc. The rail system is strapped to the rear seats as they sit folded into the floor, which provides solid mounting and easy access through the rear door. Combined with the adjustable, and outright removable, second row seats this works nicely for every bike in the house.

The main, base part of the rack is a 1.5″ x 6″ aluminum extrusion which has four slots on the larger faces. These slots are fitted with drop-in T-nuts to which the fork mounts can be fastened. To support easily repositioning the fork mounts I keep T-nuts in the unused slots, and chose ones with spring ball retention, which keeps them from rattling or sliding around while driving. While this style can be inserted or removed without removing the end caps, I keep spare ones in place because depending on the type of bike being fitted it is sometimes necessary to adjust the mount rotation or move them between slots.

Each end of the extrusion is capped with plastic face plates, and eyelets in slots on the underside are used with ratchet straps to hold the rail to the folded rear seats. A scrap of neoprene was stuck to the underside of the rail with small pieces of 3M VHB tape to help keep it from siding, and plastic corner protectors are used to keep the straps from digging into the seats.

Rocky Mounts DropTop mounts hold the bikes themselves to the rail, and the underside of each mount was covered with UHMW polyethylene tape to make side-to-side adjustment of the mounts smooth and avoid scratching the aluminum. The M6 bolts which came with the mounts were used to attach them to the T-nuts, but they are a bit long and bottomed out on the inside of the rail, so some nylon spacers are used beneath the heads of the nuts. I may eventually get some shorter bolts and avoid the spacers, but I really like the Torx head bolts that came from Rocky Mounts and I’m in no hurry to replace them. By varying the slot used the mounts can be angled at 0°, ~20°, or ~44° and can move forward or backward up to 4.5″. Coupled with easy side-to-side adjustment this makes staggering handlebars and fitting long mountain bike bars easy.

Finally, to hold the front wheels while they are off the bikes, IKEA DIMPA bags are working well. These aren’t always necessary, but with studded or potentially dirty tires and potentially setting them on other luggage, I like being able to drop the wheels into a bag before putting them in the car.

Here’s the specific parts used:

Aluminum Extrusion: TNUTZ EX-1560L-BLACK – 1.5″ x 6″ Smooth BLACK Lite T-Slotted Aluminum Extrusion (48″)
T-Nuts: TNUTZ (DB-015) 15 / 40 Series Drop-In T-Nut w/spring-ball
Eyelets: TNUTZ PULL-RING – Steel Pull Ring – M8 x 12mm
End Plates: TNUTZ 15 SERIES – BLACK PLASTIC END CAP – BLANK
Neoprene: ~5mm scrap piece from The Foam Factory
UHMW PE Tape: McMaster-Carr 76445A764
Ratchet Straps: Husky 12 ft. x 1 in. Ratchet Tie-Down Straps with S-Hook (4-Pack)
3M VHB Tape: 4910 (Spare from the Under Cabinet Lighting project.)
Nylon Spacers: McMaster-Carr 90176A155
Plastic Corner Guards: LTM Concrete CG100-000 (Amazon Link)
Wheel Storage Bags: IKEA DIMPA

Comments closed

Sunrise-like Alarm Clock via Home Assistant + Android

Bedside Sunrise Alarm Clock Setup

Quite a few years ago I came across Lighten Up!, which was a dawn-simulating alarm clock module that got connected between an incandescent lamp and used gently increasing light instead of noise. Coupled with a halogen bulb (that’d start out very yellow at lowest brightness) I had a wonderful sunrise-like alarm clock and it was much, much nicer than a beeping alarm.

The LCD displays in the Lighten Up! units began failing so I couldn’t change the programming, which was a hassle as the clocks in them drifted by a couple minutes per month. With a combination of COVID-19 remote work eliminating the need for an alarm clock and the devices dying, in the trash they went. (They also didn’t work right with LED bulbs, and now the person making them has closed down the business.)

I’ve been trying to use an alarm to stay on a more regular sleep schedule and while a bunch of other wake-up lights are available, they are dedicated units that are basically alarm clocks with built in lights. I really liked the elegance of the Lighten Up! and how it’d use an existing lamp, and outside of dedicated smart bulbs + an app I couldn’t find anything else like it. For a while I thought about developing my own hardware version that’d also work with LED bulbs, but never got around to it.

Lighten Up! (Image from Pintrest)

This winter I’ve been experimenting with Home Assistant (HA), and it turns out that with a couple cheap Zigbee parts (bulb and pushbutton from IKEA) it allows for a wonderful replacement/upgrade sunrise alarm idea. A next-generation Lighten Up!, if you will.

With everything put together the lamp next to my bed will now slowly come up to brightness 15 minutes before the wake-up alarm on my phone, reaching final as the normal alarm triggers. If I change the alarm time on my phone, or shut it off, the light-up alarm in HA will follow suit. Additionally, a physical button on the nightstand turns off the light off while replicating a sunrise alarm, or otherwise toggles the light on and off.

Even better, if I’m not home or if the alarm is set for other than between 3:00 AM and 9:00 AM (times during which I’d likely be in bed and wanting to wake up) the light won’t activate. This allows me to use alarms during the normal day for other things without activating with the light, or while traveling without waking Kristen.

Between this and the gently-increasing volume (and vibration) alarm built into the Android clock which triggers at the end of the sunrise cycle it’s a very nice, gradual wake-up system. And, all of this happens without any cloud services or ongoing subscriptions. My HA instance is local; the phone app communicates directly with it across either my home or the public networks. Communication between the physical controls and lights is a local, private network.

In this post I’ll document the major building blocks of how I did this so that someone else with basic Home Assistant experience (and a functioning HA setup, which is beyond the scope of this writeup) can do the same.

For reference, my Home Assistant hardware setup for this piece is:

With the Home Assistant Companion App for Android running on an Android phone, Home Assistant can get the date and time of the next alarm. After installing the app, go into SettingsCompanion appManage sensors and enable the Next alarm sensor. My phone is named Pixel 8, so the alarm is now available as entity sensor.pixel_8_next_alarm. Note that this is not available if an iPhone (or other iOS device) is used. (ref: Next Alarm Sensor)

Part of setting up HA configures a Zone (location) called Home. This, combined with the default location information collected by the companion app, allows HA to know if my phone is at Home (or elsewhere), via the the state of entity device_tracker.pixel_8 (eg: home).

Note: While I give YAML of the automations for configuration reference, most of these automations were built using the GUI and involve the (automatically generated) entity and device IDs. If you are setting this up you’ll want to use the GUI and build these out yourself using the code for reference.

To make this all work, three community components are used and must be installed:

Ashley’s Light Fader 2.0: This script takes a light and, over a configured amount of time, fades from the light’s current setting to the defined setting (both brightness and color temperature) using natural feeling curves (easing). It will also cancel the fade if some conditions are met. I use this to have the light fade, over 15 minutes, using a sine function, to 70% brightness and 4000K temperature, and cancel the fade if the light is turned off or brightness changes significantly, the latter of which allows the button next to the bed to cancel the alarm.

To make this happen I turn on the bulb at 1% brightness and 2202K (it’s warmest temperature), then use the script to fade to 70% and 4000K over the course of 15 minutes. This does a decent job of replicating a sunrise or the results of the Lighten Up! with a halogen bulb.

This is configured as an automation I call Bedroom Steve Nightstand: Lighten Up! (Sunrise). Note that it has no trigger because it’ll be called from the next automation:

alias: "Bedroom Steve Nightstand: Lighten Up! (Sunrise)"
description: ""
trigger: []
condition: []
action:
  - condition: state
    entity_id: light.bedroom_test_bulb_light
    state: "off"
  - service: light.turn_on
    metadata: {}
    data:
      brightness_pct: 1
      color_temp: 500
    target:
      entity_id: light.bedroom_test_bulb_light
  - service: script.1705454664908
    data:
      lampBrightnessScale: zeroToTwoFiftyFive
      easingTypeInput: easeInOutSine
      endBrightnessEntityScale: zeroToOneHundred
      autoCancelThreshold: 10
      shouldStopIfTheLampIsTurnedOffDuringTheFade: true
      shouldResetTheStopEntityToOffAtStart: false
      shouldInvertTheValueOfTheStopEntity: false
      minimumStepDelayInMilliseconds: 100
      shouldTryToUseNativeLampTransitionsToo: false
      isDebugMode: false
      light: light.bedroom_test_bulb_light
      transitionTime:
        hours: 0
        minutes: 15
        seconds: 0
      endColorTemperatureKelvin: 4000
      endBrightnessPercent: 70
mode: single

Adjustable Wake-up to Android alarm v2: This blueprint for an Automation takes the time from the next alarm sensor (alarm_source) to trigger an action before the alarm happens. I use this to initiate Ashley’s Light Fader 2.0 at 15 minutes before my alarm, only when my phone is at Home, and and the alarm is between 3:00 AM and 9:00 AM.

Part of configuring this is setting up a Helper or basically a system-wide variable, called Pixel 8 Next Alarm (entity id: input_datetime.pixel_8_next_alarm, type: Date and/or time).

This is configured as an automation called Bedroom Steve Nightstand: Lighten Up at 15 Before Alarm, set to only run if my phone is at Home and it’s between 3:00 AM and 9:00 AM:

alias: "Bedroom Steve Nightstand: Lighten Up at 15 Before Alarm"
description: ""
use_blueprint:
  path: homeassistant/adjustable-wake-up-to-android-alarm.yaml
  input:
    offset: 900
    alarm_source: sensor.pixel_8_next_alarm
    alarm_helper: input_datetime.pixel_8_next_alarm
    conditions:
      - condition: device
        device_id: 1fb6fd197bd2b771249ae819f384cfe2
        domain: device_tracker
        entity_id: e695e05f01a328b349a42bfd7d533ef6
        type: is_home
      - condition: time
        after: "03:00:00"
        before: "09:00:00"
    actions:
      - service: automation.trigger
        metadata: {}
        data:
          skip_condition: true
        target:
          entity_id: automation.lighten_up

I don’t want to get out a phone and dig into an app to manage the light, so next to the bed I have a TRÅDFRI Shortcut Button for controlling the light. If the button is pressed while the light is simulating sunrise, it turns off. If the light is off it turns it on, or visa versa.

Because turning the light off mid-dimming leaves it set at the current color and brightness, I use this instead of the normal Toggle action. In here I check the state of the bulb and either turn it off (if on), or turn it on to 100% brightness and 4000K if it is off:

alias: "Bedroom Steve Nightstand: Light Toggle"
description: >-
  Doesn't use the normal toggle because it needs to set the light color and
  brightness just in case it was left at something else when turned off
  mid-alarm.
trigger:
  - device_id: 12994a6c215ae1d4cfb86e261a2b2f3b
    domain: zha
    platform: device
    type: remote_button_short_press
    subtype: turn_on
condition: []
action:
  - if:
      - condition: device
        type: is_on
        device_id: e3421c7d54269752a371fe8443daf95f
        entity_id: 78599118c4ab8043cf03ce6532546b94
        domain: light
    then:
      - service: light.turn_off
        metadata: {}
        data:
          transition: 0
        target:
          entity_id: light.bedroom_test_bulb_light
      - stop: ""
    alias: On to Off
  - if:
      - condition: device
        type: is_off
        device_id: e3421c7d54269752a371fe8443daf95f
        entity_id: 78599118c4ab8043cf03ce6532546b94
        domain: light
    then:
      - service: light.turn_on
        metadata: {}
        data:
          color_temp: 153
          transition: 0
          brightness_pct: 100
        target:
          entity_id: light.bedroom_test_bulb_light
      - stop: ""
    alias: "Off to On: Full Brightness and 4000K"
mode: single

Finally, I also have this all displaying, and controllable, via a card stack in a dashboard. For the next alarm info I started with the template in this post but modified it to simplify one section by using now(), fix a bug in it that occurs with newer versions of HA, and then build it into something that better illustrates the start and end of the simulated sunrise. Because normal entity cards can’t do templating (to dynamically show data) I used TheHolyRoger/lovelace-template-entity-row and some Jinja templating to make it look nice.

This gives me a row which shows the next alarm time (or “No alarm” if none set), nicely formatted, and has a toggle that can enable/disable the Bedroom Steve Nightstand: Lighten Up at 15 Before Alarm automation. Finally, I added a row of buttons to allow easy toggling between 1% / 454 mireds, 33% / 357 mireds, 66% / 294 mireds, and 100% / 250 mireds so I can manually set the light to some nice presets across dawn to full brightness.

Note: There is an older version of this template in HACS, thomasloven/lovelace-template-entity-row in the Home Assistant Community Store (HACS), but it has a bug which keeps the icon from changing color to reflect the state of the automation.

type: vertical-stack
cards:
  - type: entities
    title: Bedroom
    entities:
      - type: custom:template-entity-row
        entity: automation.adjustable_wake_up_to_android_alarm
        name: Sunrise Alarm
        icon: mdi:weather-sunset-up
        active: '{{ states("automation.adjustable_wake_up_to_android_alarm"), "on") }}'
        toggle: true
        tap_action: none
        hold_action: none
        double_tap_action: none
        secondary: >-
          {% set fullformat = '%Y-%m-%d %H:%M' %}
          {% set longformat = '%a %b %-m %-I:%M %p' %}
          {% set timeformat = '%-I:%M %p' %}
          {% if states('sensor.pixel_8_next_alarm') != 'unavailable' %}
            {% set sunrise_start = state_attr('input_datetime.pixel_8_next_alarm', 'timestamp') | int %}
            {% set sunrise_end = (state_attr('sensor.pixel_8_next_alarm', 'Time in Milliseconds') /1000) | int %}
            {% if sunrise_start | timestamp_custom('%Y-%m-%d', true) == (now().timestamp() | timestamp_custom('%Y-%m-%d', true)) %}
              {% set sunrise_start_preamble = 'Today' %}
            {% elif (1+ (sunrise_start - now().timestamp() | int) / 86400) | int == 1 %}
              {% set sunrise_start_preamble = 'Tomorrow' %}
            {% elif (1+ (sunrise_start - now().timestamp() | int) / 86400) | int <= 7 %}
              {% set sunrise_start_preamble = sunrise_start | timestamp_custom('%A',true) %}
            {% else %}
              {% set sunrise_start_preamble = sunrise_start | timestamp_custom('%a %b %-m', true) %}
            {% endif %}
            {% if sunrise_end | timestamp_custom('%Y-%m-%d', true) == (now().timestamp() | timestamp_custom('%Y-%m-%d', true)) %}
              {% set sunrise_end_preamble = 'Today' %}
            {% elif (1+ (sunrise_end - now().timestamp() | int) / 86400) | int == 1 %}
              {% set sunrise_end_preamble = 'Tomorrow' %}
            {% elif (1+ (sunrise_end - now().timestamp() | int) / 86400) | int <= 7 %}
              {% set sunrise_end_preamble = sunrise_end | timestamp_custom('%A',true) %}
            {% else %}
              {% set sunrise_end_preamble = sunrise_end | timestamp_custom('%a %b %-m', true) %}
            {% endif %}
            {% if (sunrise_start_preamble == sunrise_end_preamble) %}
              {% if sunrise_start_preamble == 'None' %}
                {{ sunrise_start | timestamp_custom(longformat, true) }} - {{ sunrise_end | timestamp_custom(timeformat, true) }}
              {% else %}
                {{ sunrise_start_preamble }} {{ sunrise_start | timestamp_custom(timeformat, true) }} - {{ sunrise_end | timestamp_custom(timeformat, true) }}
              {% endif %}
            {% else %}
              {% if sunrise_start_preamble == 'None' %}
                {{ sunrise_start | timestamp_custom(longformat, true) }} - {{ sunrise_end | timestamp_custom(longformat, true) }}
              {% else %}
                {{ sunrise_start_preamble }} {{ sunrise_start | timestamp_custom(timeformat, true) }} - {{ sunrise_end_preamble }} {{ sunrise_end | timestamp_custom(timeformat, true) }}
              {% endif %}
            {% endif %}
          {% else %}
            No alarm set on {{ state_attr('device_tracker.pixel_8', 'friendly_name') }}
          {% endif %}
      - type: divider
      - entity: light.bedroom_test_bulb_light
        name: Steve's Nightstand
        icon: mdi:bed
        entity_data:
          brightness: 255
          color_temp_kelvin: 4000
    show_header_toggle: false
    state_color: true
  - type: grid
    square: false
    cards:
      - show_name: false
        show_icon: true
        show_state: false
        type: button
        tap_action:
          action: call-service
          service: light.turn_on
          data:
            brightness_pct: 1
            color_temp: 454
          target:
            entity_id: light.bedroom_test_bulb_light
        name: 1%
        icon: mdi:moon-waning-crescent
        hold_action:
          action: none
      - show_name: false
        show_icon: true
        show_state: false
        type: button
        tap_action:
          action: call-service
          service: light.turn_on
          data:
            brightness_pct: 33
            color_temp: 357
          target:
            entity_id: light.bedroom_test_bulb_light
        name: 33%
        icon: mdi:moon-last-quarter
        hold_action:
          action: none
      - show_name: false
        show_icon: true
        show_state: false
        type: button
        tap_action:
          action: call-service
          service: light.turn_on
          data:
            brightness_pct: 66
            color_temp: 294
          target:
            entity_id: light.bedroom_test_bulb_light
        name: 66%
        icon: mdi:moon-waning-gibbous
        hold_action:
          action: none
      - show_name: false
        show_icon: true
        show_state: false
        type: button
        tap_action:
          action: call-service
          service: light.turn_on
          data:
            brightness_pct: 100
            color_temp: 250
          target:
            entity_id: light.bedroom_test_bulb_light
        name: 100%
        icon: mdi:moon-full
        hold_action:
          action: none
    columns: 4

The result of all of this is that, if my phone is at home and I have an alarm set between 3:00 AM and 9:00 AM, the light next to the bed will simulate a 15-minute sunrise before the alarm goes off. If the light is simulating a sunrise, pressing the button will turn it off. Otherwise, the button toggles the light on and off at full brightness, for normal lamp-type use. Finally, via the Home Assistant UI I can easily check the status of, or turn off, the sunset alarm if I don’t want to use it.

So far, this is working great. There’s two things I’m looking into changing:

First, the bulb I’m using, 405.187.36, is an 1100 lumen maximum brightness. This is a bit too bright for the final stage of the alarm, and it’s minimum brightness is a bit higher than I’d like and seems a little abrupt. (Ideally the initial turn-on won’t be noticable.)

Since IKEA bulbs are cheap and generally work well, I’ll likely try a few other lower brightness ones and see how they work out. Both 605.187.35 (globe) and 905.187.34 (chandelier) are color temperature adjustable, 450 lumen maximum, cost $8.99, and look like good candidates as I expect their minimum brightness to be lower.

There is also 104.392.55 ($12.99), but it is fixed at 2200K and has a maximum brightness of 250 lumens. I suspect this will be nicely dim for the start, but wouldn’t allow a color transition and might not have enough final brightness to make me feel ready for the day.

I may also try something like 204.391.94 ($17.99), which is adjustable color, as this could allow me to use something like the sunrise color pallete, but this would require moving to a different script for fading. The current script doesn’t support fading between colors (see here for discussion around this), so this would take a lot of work on my part. Probably more than would be beneficial, since varying color temp on white-range bulbs is pretty darn good already.

Second, the TRÅDFRI Shortcut Button (203.563.82) that I’m using has been discontinued. It’s a nice, simple button, and I can trigger on it using short or long press. It’s replacement, SOMRIG Shortcut Button (305.603.54), isn’t in stock at my local IKEA so I don’t have one, but I expect it to be two buttons that can each have short or long presses, and perhaps even double-click on each. If so, I may add something more like dimming the nightstand light to use as a reading light, or perhaps something to leave on for the dogs when we’re gone.

Thinking a bit bigger picture I could even do things like use an in-wall dimmer to have the adjacent closet lights serve as wake-up lights. But as all the quality ones of these are Z-Wave I’d have to get another radio for the Pi and… and…

The possibilities for this stuff are nearly endless, which is neat, because it becomes an engineering problem of what to do that provides sufficient benefit without complexity for complexity’s sake. This, at least, a Home Assistant-based replacement for the old, beloved Lighten Up!, is great.

Note: This post has been updated a few times since original posting to fix grammar, a bug in the Jinga2 template for displaying the next alarm, and to add buttons for setting lamp brightness.

Comments closed

_wahoo-fitness-tnp._tcp.local

Wahoo smart trainers support network connectivity (instead of just the traditional Bluetooth or ANT+). Since I don’t have one I’d never bothered looking into how it works, but this morning while troubleshooting something with TrainerRoad running in the background I happened to see an mDNS query for _wahoo-fitness-tnp._tcp.local and realized this is how the smart trainers get discovered on the network.

Neat!

Maybe one day I’ll have a smart trainer that can use the network and I can dig further into how this all works.

Comments closed

NGINX on OPNsense for Home Assistant

I’ve been experimenting with Home Assistant (HA) for some temperature monitoring around the house. It has a great mobile client that’ll work across the public internet, but HA itself unfortunately it only does HTTP by default. It has some minor built in support for HTTPS by using the NGINX proxy and Let’s Encrypt (LE) Add-ons, but for a couple of reasons[1] I didn’t like this solution. I’m not about to expose something with credentials across the public internet via plain HTTP, so I wanted to do this proxying on my firewall instead of on the device itself.

My firewall at home runs OPNsense which has an NGINX Plugin, along with a full featured ACME client that I’m already using for other certificates, so it was perfect for doing this forwarding. After a bit of frustration, fooling around, and unexpected errors I got things working, so I wanted to share a simple summary of what it took to make it work. I’m leaving the DNS, certificate, and firewall sides of this out, as they’ll vary and are well documented elsewhere.

Here’s the steps I used:

  • Set up DNS so the hostname you wish to use is accessible internally and externally. In this example homeass.site.nuxx.net will resolve to 24.25.26.13 on the public internet, and 192.168.2.1 at home, which are the WAN and LAN interfaces on the OPNsense box.
  • Set up the ACME plugin to get a certificate for the hostname you will be using for, in this case homeass.site.nuxx.net.
  • On your Home Assistant instance, add the following to the configuration.yaml. This tells HA to accept proxied connections from the gateway. If you don’t do this, or specify the wrong trusted_proxy, you will receive a 400: Bad Request error when trying to access the site via the proxy:
http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 192.168.2.1/32
  • In OPNsense, install NGINX.
  • In ConfigurationUpstreamUpstream Server define your HA instance as a server:
    • Description: HA Server
    • Server: 192.168.2.23 (your Home Assistant device)
    • Port: 8123 (the port you have Home Assistant running on, 8123 is the default)
    • Server Priority: 1
  • In ConfigurationUpstreamUpstream define a grouping of upstream servers, in this case the one you defined in the previous step:
    • Description: Home Assistant
    • Server Entries: HA Server
  • In ConfigurationHTTP(S)Location define what will get redirected to the Upstream:
    • Toggle Advanced Mode
    • Description: Home Assistant
    • URL Pattern: /
    • Upstream Servers: Home Assistant
    • Advanced Proxy OptionsWebSocket Support: ✓
  • In ConfigurationHTTP(S)HTTP Server define the actual server to listen for HTTP connections:
    • HTTP Listen Address: Clear this out unless you want to proxy HTTP for some reason.
    • HTTPS Listen Address: 8123 and [::]:8123. Leave out the latter if you don’t wish to respond on IPv6.
    • Default Server: ✓
    • Server Name: homeass.site.nuxx.net
    • Locations: Home Assistant
    • TLS Certificate: Pick the certificate that you created early on with the ACME plugin.
    • HTTPS Only: ✓ (Unless for some reason you wish to support cleartext HTTP.)
  • Then under General Settings check Enable nginx and click Apply.
  • Finally, if needed, be sure to create the firewall rule(s) needed to allow traffic to connect to the TCP port you designated in the HTTP Server portion of the NGINX configuration.

[1] Reasons for doing the proxying on the firewall include:

  • The Let’s Encrypt Add-on won’t restart NGINX automatically on cert renewal as OPNsense can. This means I’d have to either write something to do it, or manually restart the add-on to avoid periodic certificate errors.
  • If NGINX is running on the same device as Home Assistant, then it needs to be on a different port. I prefer using the default port.
  • I’d prefer to run just one copy of NGINX on my network for reverse proxying.
  • While experimenting with NGINX and LE on HA I kept running into weird problems where something would start logging errors or just not work until I restarted the box. With everything running as containers, troubleshooting intermittent issues like these is painful enough that I preferred to avoid it.
Comments closed

DIY-ish Under Cabinet Lighting

After the kitchen was redone in our new house, the under-cabinet space (where lights would go) was intentionally left unpopulated; something for me to finish later. The electrician had fitted an outlet per cabinet grouping, and a switch on the wall that toggles those outlets, but I’d asked him to skip the lights because I really wasn’t sure what style or color we wanted, nor how I wanted to wire things. Being low voltage with the outlets and switch already fitted, it would be fairly straightforward later on.

This winter, looking for a project, I decided to finally get this done. After talking with my friend Dan and some investigating, I finally got a solution together.

What I ended up doing was sourcing a handful of parts, flexible PCB LED strips, aluminum enclosures, wire, and wall wart power supplies on Amazon, assembling it, and sticking it to the underside of the cabinets. This has worked out well, so I wanted to share what I did.

Our kitchen has three overhead cabinet groupings, the left-most has 22″ of face, the corner has 37″ of face (2x 10″ side cabinets and 1x 17″ center cabinet), and the right-most is a single 10″ cabinet. I had originally thought of ringing the underside of each cabinet with LED strip lights, but after some consideration I decided to do a single light assembly along the front edge of each cabinet, pointing down, with a wide-angle diffuser.

In the kitchen we have daylight (5000K) colored lights, but as the under-cabinet lighting is likely to be used as more of a night or secondary light, I figured on ~4000K lights. After buying strips of both 4000K and 6000K of to test, we ended up with some strips listed as Cool White 6000K. This matches pretty well whenever the overhead lights are on and it’s bright to cloudy day outside, and likely because of the grey-ish countertops it still looks fine when they are the only lights on.

It would have been possible to use a different type of strip that has multiple color temperature white LEDs and selectable color temperature, but doing this requires a controller external to adjust the light strips, which would have added around $60 just for the controllers, plus another $10 or so in LEDs, a remote control, more space taken up in the cabinets. This setup, with fixed color strips, is just AC-DC adapters plugged into the existing mains outlets, and the LED strips connected to the adapters. If we find that this isn’t good I may eventually go to controllers, but for now this is working nicely.

For the LED strips I chose FCOB LED strips from BTF-LIGHTING, 528 LEDs per meter, in 12V. With the heavy silicone coating over the LED chips it makes for a fairly even light. I chose the 12V variant of the lights because they have cut-points 22.83mm, which made it easy to make maximize the amount of LED strip under each cabinet. If the runs were longer it would have been wise to choose 24V to decrease the amperage, but at 14 watts per meter (W/m), the longest light segment will only take ~1.1 amps (A) so this should be fine. (Amazon Link)

For hookup wire I bought a simple coil of white 20 gauge copper clad aluminum (CCA) wire, again from Amazon. I already had some hookup wire, but I wanted white to blend in nicely with the cabinets. (Amazon Link)

For power supplies I picked up a basic five-pack of 12VDC / 2A wall warts. While I only needed three, the cost for five was $7 more than buying three, and since I’ll have LED strip left over and will likely build some more arrays for elsewhere in the house, I wanted spares. They also happened to come with (very cheap) barrel to screw terminal adapters that worked well enough. (Amazon Link)

Initially I was going to mount the LED strips directly to the cabinets, but Dan convinced me that even while out of sight it would look much better in an enclosure, and he was right. The project feels properly finished this way and it looks tidy. The enclosures also add additional diffusion, making the light look smoother and more even and overall better. For this I chose some 1m long pieces of aluminum channel with a 60° milky white domed cover, then cut it to width for each cabinet section. (Amazon Link)

The light enclosures include screws and mounting clips, but I instead opted for tape, and picked up a 5 yard roll of 3M VHB 4910, which is specifically described as being good for Polycarbonate, Aluminum, and Acrylic/Polyurethane Paint; perfect for sticking these housings to the underside of cabinets. (Amazon Link)

Assembly was done by first determining the width of the housings, so I went with even-inch sizes that are just slightly narrower than the underside of the cabinet. This resulted in five pieces: 1x 22″, 1x 16-1/2″, and 3x 10-1/4″. With the cover snapped into the aluminum channel it all cut clean and easily using a circular miter saw with a fine tooth carbide blade.

I then cut LED strip to the next step shorter than the housing, soldered the supply wire, stuck the strip into the housing via it’s self-adhesive backing, fitted the endcaps, added hot-melt glue for strain relief at the solder area, and snapped the face on. For the corner cabinet lights I wired it in a T arrangement, with the two sides being fed from the center, and then a wire out of the center to head up to the cabinet. This arrangement was because the corner cabinet holds the outlet near the back left side and it minimizes wire distance. It may have also helped with voltage drop on a long-ish strip of LEDs, but I’m not sure the ~30″ total would be enough to actually cause a problem.

A small hole (3/16″) was drilled in the underside of the cabinet to run the wire up to the supply, cabinet undersides cleaned and strips stuck in place with VHB tape, wire was tacked in place with hot melt glue, and screw terminal to barrel adjuster adapters (which came with the power supplies) fitted. A bit of cable tying to tidy the cables in the cabinets and it’s all done.

Total cost for this project was $122.83, and that includes the unused 4000K strip that I bought as a test. There is enough extra left over to do at least another 10′ of enclosed light somewhere else in the house, so I might build some light strips for under my workbench shelves or perhaps where I have music gear in my closet.

Kristen and I are both really happy with how it came out. More than just filling in the room and serving as a night light it actually offers usable additional light when working on the counters, especially when cooking at the stove.

Comments closed

Surprise New Bike Day: 2023 Salsa Warbird C GRX 600 1x

It’s been no secret that for years my favorite drop bar bike was my beloved 2019 Salsa Warbird Carbon 105 700. This bike has been with me on some of my most memorable rides, from remote parts of the UP and Canadian wilderness to silly northern LP bike path routes, from single track where it didn’t quite belong to long summertime wanders on dirt roads.

For years, and especially in 2023 after switching it to 1x, I’ve told numerous folks that there’s nothing I’d change on it and no reason to get anything new; it’s simply excellent. But then in late November while cleaning it up for winter I found a bit of a shock: a small starburst crack in the frame at the top tube/seat tube junction and a couple other hairline cracks on the seat tube. As things wear out and everything eventually fails I can’t say I was devastated, but I really was disappointed. I loved riding that bike and did not want to change anything. I even came to really like the white color.

After finding the crack I rode it a couple more times and things seemed fine. I have a long seatpost which extended well below the crack area, and I didn’t feel or hear anything when riding, but like any crack it’s best to be safe. The warranty for a carbon frame is five years, which was coming up, so I sent photos of the crack to the folks over at the venerable Tree Fort Bikes and Salsa to get things rolling. I wasn’t sure if it was problematic, and — honestly — I really didn’t want to replace the frame if it wasn’t needed, but I wanted to ask. I love how my Warbird looks with the white frame and reflective black vinyl that I added, and I wasn’t exactly keen on a matte black replacement, re-running brake lines, etc. And, it rode great!

After just a handful of questions Craig emailed me with a massive surprise: Salsa is replacing my 2019 Warbird (v4) frame with a complete 2023 Warbird C GRX 1x bike, and instead of being a simple black warranty replacement it’s a nice clay-ish grey color! This was better than I could have possibly hoped for, because not only did it replace the problematic frame, it moved me to Shimano’s GRX drivetrain! And I also wouldn’t have to cut and re-run the brake lines, etc.

Back when my Warbird came out GRX wasn’t available, so it came with the (very good) 105 R7000 road groupset. This is an amazingly good drivetrain, but for rougher roads I switched to the Ultegra RD-RX800 derailleur, which was basically a high-end road derailleur with a clutch, originally intended for cyclocross use. Then in late 2022 I used a few Wolf Tooth Components parts and made it a sorta-hacky yet very functional 1x drivetrain because I wanted to get away from the problems inherent with 2x and riding in poor conditions, notably gunking up a front derailleur.

This setup worked great, but I felt a bit limited by maximum cassette size (40t) and my left brifter had a disconnected shift lever, which would rattle around on chattery roads. Minor, I know, and while I was proud of the semi-hacky drivetrain, improvements such as a full GRX drivetrain would have been nice, but I couldn’t justify it when things worked so well. But suddenly now I had it!

Last week I picked up the bike from Tree Fort, and over some unseasonably rainy, cold, and blah afternoons I shuffled parts around and now it’s ready. My new gravel bike, a 2023 Salsa Warbird C GRX 1x with a few upgrades!

Build

Frame / Fork: 2023 Salsa Warbird Carbon / Salsa Waxwing (Light Grey)
Wheelset: Specialized Roval Terra C
Ratchets: DT Swiss HWTXXX00NSK54S (54T)
Tires: Specialized Pathfinder Pro 2Bliss Ready (700×42, Black Sidewall)
Crank: Shimano FC-RX810-1 (42T, 172.5mm)
Bottom Bracket: Shimano SM-BB72-41B
Cassette:
 Shimano CS-M8000 (11-42)
Right Shift/Brake Levers:
Shimano ST-RX600-R
Left Shift/Brake Levers:
Shimano BL-RX600-L
Brake Calipers:
Shimano BR-RX400
Brake Rotors: Shimano SM-RT64 (160mm)
Brake Pads:
Shimano K05S-RX (Resin)
Chain:
Shimano CN-HG601-11
Rear Derailleur:
Shimano RD-RX812
Bar Tape:
MSW HBT-300 Anti-Slip Gel+ (Black)
Handlebar: Salsa Cowbell Deluxe (44cm)
Headset: Cane Creek Hellbender 70 (IS41/28.6/H9 | IS52/40)
Stem: Thomson Elite X4 (SM-E139 10° X 100mm X 31.8 1-1/8 X4 Black)
Spacers: Generic Aluminum
Stem Cap: MASH Donut 2.0
Seatpost: Thomson Elite (SP-E113SB 27.2 X 410 Setback, Black)
Seatpost Clamp: Salsa Lip Lock
Saddle: Specialized Power Expert (143mm, Black)
Pedals: Shimano PD-M8100
Bottle Cages: Specialized Zee Cage II (2x Left, 2x Right)
Bottle Cage Screws: McMaster-Carr 94500A233 (316 Stainless, M5 x 0.8mm, 20mm)
Front Light: Outbound Lighting Detour
Rear Light: Garmin Varia RTL515
Rear Light Mount: Garmin Varia Seat-post Quarter Turn Mount
Bell: RockBros Bell (Black)
Computer: Garmin Edge 840
Computer Mount: SRAM Quickview Computer Mount
Sensors: Garmin Bike Speed Sensor (Front Wheel), Garmin Cadence Sensor 2 (Crank)
Anti-Rub Tape: McMaster-Carr 76445A764 (Low-Friction UHMW Tape, 0.0115″ Thick, 2″ Wide)
Mounting Hole Plugs: Heyco 2590
Top Tube Bag: Revelate Designs Mag-Tank Bolt-On
Saddle Bag: Lezyne Road Caddy
Frame Pump: Lezyne Sport Drive HP
Derailleur Hanger: 465 / QBP FS2322

Weight

Total weight for the bike, with everything but bottles (including lights, pump, saddle bag+tools, and computer), is 22.54 pounds. Removing the computer/lights/saddle bag/pump brings it down to 20.78 pounds, so I expect that without pedals, cages, or mounts (the usual way of weighing a bike) it’d be in the 19-pound range.

Build Choices:

Crank/Cassette Upgrade: After moving my previous Warbird to 1x a year ago I realized that I like having a 42t front ring. Since the bike came with a 40t ring I wanted to upgrade that, but it turned out that I could get a complete FC-RX810 crankset with a 42t ring for not much more than a stand-alone ring. Swapping from the stock FC-RX610 swap saved 84g while increasing the chainring size, makes mounting a cadence sensor easier, and opens up the possibility of getting a power meter on the bike.

At the same time I ordered an CS-M8000 cassette to replace the stock CS-M5100, which saved another 114g. While I originally was going to get an 11-40 to match my previous Warbird, my friend Ray convinced me that a 42, ending up with an even 1:1 in the lowest gear would be good, and I agreed. Between the two sizes the seven lowest cogs are the same, so typical flat/rolling stuff would feel the same with either, but when I do need a climbing/trail gear it’ll be there.

Chain Drop Protection: I was originally going to fit some chain drop protection, like the Wolf Tooth LoneWolf, but after thinking about how many times I’ve dropped a chain in the past, I opted against it. Mounting this would also require fitting a front derailleur mount which makes bottle cage mounting more fiddly and makes the bike harder to clean. I may still fit this later on, but for now I’m content continuing without.

Rotors: I am generally very fond of Shimano rotors with solid aluminum center carriers as they seem to be harder to bend and have an aluminum core (Ice Technologies) to help with heat dissipation. The bike came with some SM-RT64, and while they are a bit heavier per-rotor (~25g) than others, and just steel, it would have cost a fair bit (~$100) to replace them. I have similar rotors on my fatbike and they’ve been working well, so for now I’m going to stick with these rotors and see how it goes. They can always be upgraded later.

Frame Pump/Saddle Bag: On the previous Warbird I used a somewhat large Specialized saddle bag with a tiny 4″ pump tucked inside next to the tube. This worked well, but I began having problems with the pack Velcro no longer holding, so I also had a releasable cable tie holding it to the saddle.

I’d also never needed — that is tested — the tiny pump in the field, so out of an abundance of caution (and some paranoia) I’d often tuck a second, larger pump in my jersey pocket for long rides. This other pump had been used a few times, so with the bike swap I’ve moved to mounting the beloved (and cheap) Lezyne Sport Drive HP to the frame behind the seat tube bottle cage. I’m wary of road spray causing problems with the pump, but if it does I’ll just start carrying it in my pocket.

By no longer needing room for a pump in the bag I was able to swap to the Lezyne Road Caddy, a small and elegant seat bag that I’ve had on my road bike for a couple years.

Bike Fit: This and my road bike, a custom built Salsa Warroad, are very very similar in geometry, but I’ve had it set up with the bars slightly lower than on the Warbird. Using my favorite stem comparison tool I found that by removing 10mm of spacers below the stem I can get the bar clamp to a nearly identical position on both bikes, so as a bit of an experiment I’m giving this a go, leaving the steerer tube uncut so I can go back if desired.

I’m slightly concerned about the fit when riding more technical trails, and I may have a harder time keeping my forearms near level while on rough surface, but it’s plenty easy to go back if needed.

Bottle Cage Screws: When using Specialized Zee Cages it’s important to have a low profile screw head, else they’ll rub on the bottle and make it hard to insert. The screws which come with the cages are a nice shape, but are a chromed steel that seems to corrode with sweat, sports drink, and road treatment chloride, so I prefer something else. I prefer something like 316 stainless, and I had some 20mm of these laying around from a previous project. For just-bottle-cages this is longer than the needed ~15mm, but the additional mass is across the three standard cages is only 6x 1/4 of the mass of a single screw (1.5x a single screw), or ~5g. It wasn’t worth spending $11+shipping to save that little mass.

Anti-Rub Tape on Head Tube: On my previous Warbird, and on the Warroad, I shortened the front brake hose so it’d take a clean path from the fork to the bar, not touching the head tube. This works, but also gets it the way of the light mount, and makes adjusting spacers difficult because I have to remove the bar from the stem to slide things upward.

The stock hose length on this model rubs the front of the head tube, but I’m not sure I want to shorten it yet. For now I simply put a strip of UHMW PE tape along the front of the head tube, below the Salsa logo, so the hose won’t rub on the frame/paint/carbon. Once I settle on spacers and work a bit more on accessory mounting I may shorten the hose, or I may just leave this alone.

Comments closed

Subaru Outback Cabin Air Filter Airflow Problems

With autumn rolling in the HVAC blower in my Subaru Outback seemed to be acting up. Whenever I’d have the blower at a low speed for basic air circulation it just… didn’t seem to be doing much. It turns out this was caused by the cabin air filter. The heavier filter, the Breathe Easy seen on the right, was restricting flow so much that I’d only get noticeable air movement when the fan was at ~2/3 of maximum or higher.

I’d swapped this filter in over the summer, and at first I didn’t notice, because I normally run the air conditioning (cooling) on a pretty high speed or have the windows down with the blower off. With temperatures dropping I’ll often have fresh air flowing in through the vents, but with the fan at a low speed, just to keep a fresh-air feeling inside the car. On the normal low (one or two bar) setting I just… wasn’t feeling much.

My thoughts first went to problems with an HVAC damper, or perhaps a motor speed controller, but then I remembered the activated charcoal cabin filter I’d fitted and wondered if maybe it was so restricting that the blower wasn’t working right.

Yep, that was the problem. I fitted up a simple Denso paper filter for capturing pollen and everything is working fine again. I’m glad it wasn’t anything more expensive.

Comments closed