Sunrise and Sunset in C++

I wanted to calculate sunrise and sunset at my location for a recent project I was working on where I wouldn’t necessarily have access to internet connectivity. I did some quick searching and found a Wikipedia page with equations and thought it would be straightforward to put those equations into code. Somehow, I was not able to get the routines working correctly.

I came across a NOAA page that used equations in a different form, and also had links to an excel spreadsheet I could download and play with. I was able to reverse engineer the spreadsheet into C++ code to get a working routine that calculates the sunrise and sunset to an accuracy that is good enough for my needs.

I’m sharing my code here. If you see anything I’ve done wrong, please let me know. If you find it useful, please let me know about that as well.

/////////////////////////////////////////////////////////////////////////////
// From NOAA Spreadsheet https://gml.noaa.gov/grad/solcalc/calcdetails.html
bool getSunriseSunset(time_t& Sunrise, time_t& Sunset, const time_t& TheTime, const double Latitude, double Longitude)
{
	bool rval = false;
	struct tm LocalTime;
	if (0 != localtime_r(&TheTime, &LocalTime))
	{
		// if we don't have a valid latitude or longitude, declare sunrise to be midnight, and sunset one second before midnight
		if ((Latitude == 0) || (Longitude == 0))
		{
			LocalTime.tm_hour = 0;
			LocalTime.tm_min = 0;
			LocalTime.tm_sec = 0;
			Sunrise = mktime(&LocalTime);
			Sunset = Sunrise + 24*60*60 - 1;
		}
		else
		{
			double JulianDay = Time2JulianDate(TheTime); // F
			double JulianCentury = (JulianDay - 2451545) / 36525;	// G
			double GeomMeanLongSun = fmod(280.46646 + JulianCentury * (36000.76983 + JulianCentury * 0.0003032), 360);	// I
			double GeomMeanAnomSun = 357.52911 + JulianCentury * (35999.05029 - 0.0001537 * JulianCentury);	// J
			double EccentEarthOrbit = 0.016708634 - JulianCentury * (0.000042037 + 0.0000001267 * JulianCentury);	// K
			double SunEqOfCtr = sin(radians(GeomMeanAnomSun)) * (1.914602 - JulianCentury * (0.004817 + 0.000014 * JulianCentury)) + sin(radians(2 * GeomMeanAnomSun)) * (0.019993 - 0.000101 * JulianCentury) + sin(radians(3 * GeomMeanAnomSun)) * 0.000289; // L
			double SunTrueLong = GeomMeanLongSun + SunEqOfCtr;	// M
			double SunAppLong = SunTrueLong - 0.00569 - 0.00478 * sin(radians(125.04 - 1934.136 * JulianCentury));	// P
			double MeanObliqEcliptic = 23 + (26 + ((21.448 - JulianCentury * (46.815 + JulianCentury * (0.00059 - JulianCentury * 0.001813)))) / 60) / 60;	// Q
			double ObliqCorr = MeanObliqEcliptic + 0.00256 * cos(radians(125.04 - 1934.136 * JulianCentury));	// R
			double SunDeclin = degrees(asin(sin(radians(ObliqCorr)) * sin(radians(SunAppLong))));	// T
			double var_y = tan(radians(ObliqCorr / 2)) * tan(radians(ObliqCorr / 2));	// U
			double EquationOfTime = 4 * degrees(var_y * sin(2 * radians(GeomMeanLongSun)) - 2 * EccentEarthOrbit * sin(radians(GeomMeanAnomSun)) + 4 * EccentEarthOrbit * var_y * sin(radians(GeomMeanAnomSun)) * sin(2 * radians(GeomMeanLongSun)) - 0.5 * var_y * var_y * sin(4 * radians(GeomMeanLongSun)) - 1.25 * EccentEarthOrbit * EccentEarthOrbit * sin(2 * radians(GeomMeanAnomSun))); // V
			double HASunriseDeg = degrees(acos(cos(radians(90.833)) / (cos(radians(Latitude)) * cos(radians(SunDeclin))) - tan(radians(Latitude)) * tan(radians(SunDeclin)))); // W
			double SolarNoon = (720 - 4 * Longitude - EquationOfTime + LocalTime.tm_gmtoff / 60) / 1440; // X
			double SunriseTime = SolarNoon - HASunriseDeg * 4 / 1440;	// Y
			double SunsetTime = SolarNoon + HASunriseDeg * 4 / 1440;	// Z
			LocalTime.tm_hour = 0;
			LocalTime.tm_min = 0;
			LocalTime.tm_sec = 0;
			time_t Midnight = mktime(&LocalTime);
			Sunrise = Midnight + SunriseTime * 86400;
			Sunset = Midnight + SunsetTime * 86400;
		}
		rval = true;
	}
	return(rval);
}

I’ve included the individual routines I created when trying to duplicate the Wikipedia equations. If anyone can point out why they didn’t work for me, I’d appreciate that as well.

/////////////////////////////////////////////////////////////////////////////
double radians(const double degrees)
{
	return((degrees * M_PI) / 180.0);
}
double degrees(const double radians)
{
	return((radians * 180.0) / M_PI);
}
double Time2JulianDate(const time_t& TheTime)
{
	double JulianDay = 0;
	struct tm UTC;
	if (0 != gmtime_r(&TheTime, &UTC))
	{
		// https://en.wikipedia.org/wiki/Julian_day
		// JDN = (1461 × (Y + 4800 + (M ? 14)/12))/4 +(367 × (M ? 2 ? 12 × ((M ? 14)/12)))/12 ? (3 × ((Y + 4900 + (M - 14)/12)/100))/4 + D ? 32075
		JulianDay = (1461 * ((UTC.tm_year + 1900) + 4800 + ((UTC.tm_mon + 1) - 14) / 12)) / 4
			+ (367 * ((UTC.tm_mon + 1) - 2 - 12 * (((UTC.tm_mon + 1) - 14) / 12))) / 12
			- (3 * (((UTC.tm_year + 1900) + 4900 + ((UTC.tm_mon + 1) - 14) / 12) / 100)) / 4
			+ (UTC.tm_mday)
			- 32075;
		// JD = JDN + (hour-12)/24 + minute/1440 + second/86400
		double partialday = (static_cast<double>((UTC.tm_hour - 12)) / 24) + (static_cast<double>(UTC.tm_min) / 1440.0) + (static_cast<double>(UTC.tm_sec) / 86400.0);
		JulianDay += partialday;
	}
	return(JulianDay);
}
time_t JulianDate2Time(const double JulianDate)
{
	time_t TheTime = (JulianDate - 2440587.5) * 86400;
	return(TheTime);
}
double JulianDate2JulianDay(const double JulianDate)
{
	double n = JulianDate - 2451545.0 + 0.0008;
	return(n);
}
/////////////////////////////////////////////////////////////////////////////
// These equations all come from https://en.wikipedia.org/wiki/Sunrise_equation
double getMeanSolarTime(const double JulianDay, const double longitude)
{
	// an approximation of mean solar time expressed as a Julian day with the day fraction.
	double MeanSolarTime = JulianDay - (longitude / 360);
	return (MeanSolarTime);
}
double getSolarMeanAnomaly(const double MeanSolarTime)
{
	double SolarMeanAnomaly = fmod(357.5291 + 0.98560028 * MeanSolarTime, 360);
	return(SolarMeanAnomaly);
}
double getEquationOfTheCenter(const double SolarMeanAnomaly)
{
	double EquationOfTheCenter = 1.9148 * sin(radians(SolarMeanAnomaly)) + 0.0200 * sin(radians(2 * SolarMeanAnomaly)) + 0.0003 * sin(radians(3 * SolarMeanAnomaly));
	return(EquationOfTheCenter);
}
double getEclipticLongitude(const double SolarMeanAnomaly, const double EquationOfTheCenter)
{
	double EclipticLongitude = fmod(SolarMeanAnomaly + EquationOfTheCenter + 180 + 102.9372, 360);
	return(EclipticLongitude);
}
double getSolarTransit(const double MeanSolarTime, const double SolarMeanAnomaly, const double EclipticLongitude)
{
	// the Julian date for the local true solar transit (or solar noon).
	double SolarTransit = 2451545.0 + MeanSolarTime + 0.0053 * sin(radians(SolarMeanAnomaly)) - 0.0069 * sin(radians(2 * EclipticLongitude));
	return(SolarTransit);
}
double getDeclinationOfTheSun(const double EclipticLongitude)
{
	double DeclinationOfTheSun = sin(radians(EclipticLongitude)) * sin(radians(23.44));
	return(DeclinationOfTheSun);
}
double getHourAngle(const double Latitude, const double DeclinationOfTheSun)
{
	double HourAngle = (sin(radians(-0.83)) - sin(radians(Latitude)) * sin(radians(DeclinationOfTheSun))) / (cos(radians(Latitude)) * cos(radians(DeclinationOfTheSun)));
	return(HourAngle);
}
double getSunrise(const double SolarTransit, const double HourAngle)
{
	double Sunrise = SolarTransit - (HourAngle / 360);
	return(Sunrise);
}
double getSunset(const double SolarTransit, const double HourAngle)
{
	double Sunset = SolarTransit + (HourAngle / 360);
	return(Sunset);
}

Here’s the code snippet I was using when trying to get the Wikipedia equations working.

double JulianDay = JulianDate2JulianDay(Time2JulianDate(LoopStartTime));
double MeanSolarTime = getMeanSolarTime(JulianDay, Longitude);
double SolarMeanAnomaly = getSolarMeanAnomaly(MeanSolarTime);
double EquationOfTheCenter = getEquationOfTheCenter(SolarMeanAnomaly);
double EclipticLongitude = getEclipticLongitude(SolarMeanAnomaly, EquationOfTheCenter);
double SolarTransit = getSolarTransit(MeanSolarTime, SolarMeanAnomaly, EclipticLongitude);
double DeclinationOfTheSun = getDeclinationOfTheSun(EclipticLongitude);
double HourAngle = getHourAngle(Latitude, DeclinationOfTheSun);
double Sunrise = getSunrise(SolarTransit, HourAngle);
double Sunset = getSunset(SolarTransit, HourAngle);
std::cout.precision(std::numeric_limits<double>::max_digits10);
std::cout << "         Julian Date: " << Time2JulianDate(LoopStartTime) << std::endl;
std::cout << "           Unix Time: " << LoopStartTime << std::endl;
std::cout << "         Julian Date: " << timeToExcelLocal(JulianDate2Time(Time2JulianDate(LoopStartTime))) << std::endl;
std::cout << "            Latitude: " << Latitude << std::endl;
std::cout << "           Longitude: " << Longitude << std::endl;
std::cout << "          Julian Day: " << JulianDay << std::endl;
std::cout << "       MeanSolarTime: " << MeanSolarTime << std::endl;
std::cout << "    SolarMeanAnomaly: " << SolarMeanAnomaly << std::endl;
std::cout << " EquationOfTheCenter: " << EquationOfTheCenter << std::endl;
std::cout << "   EclipticLongitude: " << EclipticLongitude << std::endl;
std::cout << "        SolarTransit: " << timeToExcelLocal(JulianDate2Time(SolarTransit)) << std::endl;
std::cout << " DeclinationOfTheSun: " << DeclinationOfTheSun << std::endl;
std::cout << "           HourAngle: " << HourAngle << std::endl;
std::cout << "             Sunrise: " << timeToExcelLocal(JulianDate2Time(Sunrise)) << std::endl;
std::cout << "              Sunset: " << timeToExcelLocal(JulianDate2Time(Sunset)) << std::endl;

Perils of Open Tabs

I have had several tabs open in my browser for an extended period of time, across multiple reboots of my computer, and connecting to several different networks.

Today I was traveling and happened to be using the WiFi in the Amex Centurion Lounge. Two of the background tabs in my browser reported privacy errors, and when I tried to go to them anyway, they were redirected to an ATT hotspot error page. What’s really frustrating is that now, I don’t know what one of the original URLS was.

Initial message when page didn’t load
Web Page Blocked

Both pages were hosted on opensource.com and I’m pretty sure were related to GIT, possibly with a group of handy commands to remember.

Govee H5182 Dual Probe Bluetooth Meat Thermometer

Govee H5182

Because I can’t seem to stop playing with the Govee line of Bluetooth thermometers, when this model came on sale for less than $20 I added it to my Amazon cart.

I knew that the Bluetooth protocol would be different purely based on multiple sensors, but I wanted to play with it anyway. With less than a month since I’d previously figured out a device, I was able to figure this one out in less than an hour, though I still don’t know the battery strength indicator in either this or the H5183 I decoded last month. I’ve added the code to my monitoring program https://github.com/wcbonner/GoveeBTTempLogger/  but have not yet published the changes. The existing code is designed around a single temperature, humidity, and battery reading per device. Even the H5183 broke the model slightly because it has two temperatures, current and alarm, and no humidity. I’m rethinking my data storage to be able to be more expandable, while still being backwards compatible as far as the log files, and will publish the new code when I’ve got it working.

This unit has the advantage of a display over the H5183. It can be used without connecting to a phone at all. You can turn it on, set the alarms, and monitor what’s going on. The one thing I did not figure out how to do using the buttons was to change the display from °F to °C, which I wanted to do to simplify debugging. Using the app I was able to update the settings and view the details.

Details from Amazon:

  • Remote Monitoring: With this wireless grill meat thermometer, you can spend less time waiting and more time multitasking within a 230ft Bluetooth range, Pull out your phone and check the meat temperatures of your grill, smoker, and oven at any time
  • Meat Temperature Settings: Govee Home App offers USDA-recommended food temperatures with various doneness levels for foods such as beef, lamb, chicken, pork, turkey, and more, Grill more efficiently, whether you’re a beginner or a professional
  • Smart Alerts: Avoid the risk of overcooking your food, Once your preset temperature is out of range, your meat thermometer will beep and send an instant notification to your phone via the Govee Home App
  • Food Grade Probes: Our food grade 304 stainless steel thermometer probes have an accuracy of ±1.8°F/1°C to reliably monitor the doneness of your food (under 302℉) ,*Please remember not to touch the probes immediately after use
  • Easy to Place: This Bluetooth meat thermometer has a strong magnetic backing and a folding stand to easily set up on the grill, smoker and oven when barbecuing, There is also a large backlight screen to make it easier to read at night

There is also a single probe version of this thermometer. I expect it works similar to this unit, but the price savings made it less interesting to me since I already have the H5183 in my kitchen.

Govee H5181

Govee H5074 and VPD data

I was playing with a couple of brand new H5074 devices and realized that the iOS app now includes two new charts, Dew Point, and VPD.

VPD

The new devices report as hardware version 2.00.01 firmware version 1.00.01. My older devices all report hardware version 1.00.01 and firmware version 1.01.00.

The pressure and dew point are available in both the new and old H5074 devices, but not with any of the other units I’ve got connected. (H5177 or H5075)

I’ve not been able to find details on the data. I don’t know if it’s packed into the BLE advertising data, or only available in the connected download data.

Govee H5183 Bluetooth Wireless Meat Thermometer

GVH5183

I recently came across the Govee Meat Thermometer on sale at amazon and decided to give it a try and see if the communication protocol was similar to any of the other Govee thermometers I’ve bought, the H5177, H5075 or H5074.

The Bluetooth communication protocol is different from any of the other devices I’ve got, but after a day of staring at raw data I was able to figure out some of the details and add support to my monitoring program https://github.com/wcbonner/GoveeBTTempLogger/ . The Bluetooth announcements from the device include both the current temperature and the set alarm temperature. I’ve not yet figured out the battery strength data. The phone app displays the battery, so I know it should be available.

There’s an orange button on one side to turn the unit on. Hold for three seconds. It will beep indicating it’s on. The LED will start flashing green, and the device will periodically send Bluetooth announcements including the temperature and alarm temperature. If you connect to the device with the phone app, the LED will switch to flashing blue, indicating that it’s in a connected state. While the device is in a connected state, it doesn’t not send out announcements. To return it to standard mode, simply exit back to the top level of the app. The app will still alarm when the probe gets to set temperature. Holding the button for three seconds when it’s on will turn it off, with beeps to confirm the change.

A nice feature of this device is that it has a magnet built in, enough to hold the device to the front of a metal oven.

Details from the Amazon listing:

  • Useful Smart Alerts: If temperatures fall out of your preset range, an alarm will sound, and you will get a phone alerts notification via the Govee Home app. The probe measuring range is 0° to 300°C /32° to 572°F. Note: press and hold the orange button for 3 seconds to power on.
  • Convenient Remote Monitoring: Tired of waiting near a hot grill, With a 230ft/70m smart Bluetooth wireless control range(no obstructions), you are free to relax and check your temperatures on your smartphone at a glance. Remember to remove the protective tip before use.
  • Performance Review: Detailed temperature data and easy-to-read charts are generated within 2 hours. (Charts can’t be stored/downloaded) Perfect for a quick review or an in-depth analysis of temperature performance. Improve your cooking and temperature with calibration at ±5°C.
  • Temperature Made Easy: 28 temperature recommendations for 14 types of foods take the hassle out of cooking. Ideal for both beginner cooks and pro chefs.
  • Practical Features: Temperature switching between Fahrenheit and Celcius. (The default unit is Fahrenheit) Mute alarm function and countdown timer on the Govee Home app. The magnetic back can easily be attached to the refrigerator, oven or grill, or any other metal surface. Note: Please keep the meat thermometer unit safe from heat sources and very hot surfaces to protect its internal batteries and exterior shell.
  • Part Number: B5183011

Raspberry Pi GPSD with Pepwave MAX Transit

I’ve been wanting to do some GPS data programming with the Raspberry Pi that’s on my boat. The Pi is connected to the NMEA 2000 network, and so should be able to retrieve GPS coordinates from either my chartplotter or my AIS unit when they are powered on, but it should also be able to get the GPS data from my Max Transit cellular gateway device.

It turns out that configuring gpsd to retrieve the data from the max transit was fairly easy. I edited the file /etc/default/gpsd to include the internal address and port of my router and restarted gpsd and now the Pi has the correct location.

/etc/default/gpsd

The devices section was initially empty. I added tcp://192.168.50.1:60660 between the pair of double quotes. After that, I was able to run gpsmon with no parameters and it connects to the local machine and reports the gps statistics.

gpsmon

I’d verified that I can read the device directly over the network with the command gpsmon 192.168.50.1:60660 but I wanted to be able to write my programs without needing to know where the gps was located.

gpsmon

SVG, CSS Style, dark-mode, and Apple

In my recent GoveeBTTempLogger Project I’ve been writing SVG files as temperature graphs. I realized I could organize the color combinations with CSS Style descriptions inside the SVG instead of fully describing each element of the graphic. I am using transparent backgrounds by default, and loading the SVG files in a simple index html file like any other image.

Playing with CSS in the HTML, I came across the ability to support dark-mode. I thought that this would be really nice for when I bring up the graphs on my phone late at night, not having everything being in a bright white background.

    @media screen and (prefers-color-scheme: dark) {
        body {
            background-color: black;
            color: grey;
        }
    }

I added the previous block to my style in the index.html file, which overrides the default body style of background-color: white and color: black. It works nicely at the top level, but now the black lines and text in my SVG became invisible, since now they match the background.

I tried something similar in the style for my SVG and it was working great in Chrome or Microsoft Edge. But then I realized that it doesn’t work on my phone. After realizing that it wasn’t working correctly on the phone, I decided to try switching the defaults so my default used grey, and if light mode was selected it would use black. It still didn’t work! It seems that apple is paying attention to my color-scheme selection, but doing something different from any other browser.

	<style>
		text { font-family: sans-serif; font-size: 12px; fill: grey; }
		line { stroke: grey; }
		polygon { fill-opacity: 0.5; }
	@media screen and (prefers-color-scheme: light) {
		text { fill: black; }
		line { stroke: black; }
	}
	</style>

I had a friend who works primarily on a Mac test to see if the problem was specific to the iPhone, or if it occurred in Safari on the desktop as well. Yes. Apple is handling my styles differently from the other browsers I tried.

Safari on Mac
Chrome on Mac
Firefox on Mac

You can see my choices displaying the way I want on Chrome and Firefox, with grey lines and text on a black background, while Safari isn’t showing the grey. Am I doing something explicitly wrong in my style that Chrome and Firefox are forgiving? Or is Apple not supporting the feature properly?

The frustrating thing is that all browsers on iPhones use the Apple rendering engine. That means there’s no way I know to get the functionality I want on my phone, where the phone uses dark mode overnight, but light mode during the day.

Govee H5177 Bluetooth Low Energy Smart Thermo-Hygrometer

After playing with Govee thermometers H5074 and H5075, I came across the H5177 model with similar specs but with a larger display including some buttons and backlight. I decided to give it a try, just to see if it used the same protocol as the previous units.

wim@WimPi4-dev:~ $ sudo ../visualstudio/projects/GoveeBTTempLogger/bin/ARM/Debug/GoveeBTTempLogger.out -v 2 | grep E3:5E:CC:21:5C:0F
 [2020-11-09T00:26:34] 44 E3:5E:CC:21:5C:0F 06 (UUID) 0A18F5FE88EC (Name) Govee_H5074_5C0F
 [2020-11-09T00:26:34] 26 E3:5E:CC:21:5C:0F 88EC000103DE106402 (Temp) 45.842°F (Humidity) 43.18% (Battery) 100%
 [2020-11-09T00:26:35] 44 E3:5E:CC:21:5C:0F 06 (UUID) 0A18F5FE88EC (Name) Govee_H5074_5C0F
 [2020-11-09T00:26:35] 42 E3:5E:CC:21:5C:0F 4C000215494E54454C4C495F524F434B535F485750740F5CC2 (Apple) (UUID) 494E54454C4C495F524F434B535F4857 (Major) 5074 (Minor) 0F5C (RSSI) C2
 ^C* SIGINT: Caught Ctrl-C, finishing loop and quitting. *
 GoveeBTTempLogger Version 1.20201016-1 Built on: Oct 16 2020 at 12:13:28 (exiting)
 wim@WimPi4-dev:~ $ sudo ../visualstudio/projects/GoveeBTTempLogger/bin/ARM/Debug/GoveeBTTempLogger.out -v 2 | grep A4:C1:38:13:AE:36
 [2020-11-09T00:27:05] 46 A4:C1:38:13:AE:36 GVH5075_AE36 (UUID) 88EC (Flags) 05 (Manu) 88EC0002859F4900 (Temp) 61.7502°F (Humidity) 27.9% (Battery) 73%
 [2020-11-09T00:27:05] 42 A4:C1:38:13:AE:36 4C000215494E54454C4C495F524F434B535F48575075F2FFC2 (Apple) (UUID) 494E54454C4C495F524F434B535F4857 (Major) 5075 (Minor) F2FF (RSSI) C2
 ^C* SIGINT: Caught Ctrl-C, finishing loop and quitting. *
 GoveeBTTempLogger Version 1.20201016-1 Built on: Oct 16 2020 at 12:13:28 (exiting)
 wim@WimPi4-dev:~ $ sudo ../visualstudio/projects/GoveeBTTempLogger/bin/ARM/Debug/GoveeBTTempLogger.out -v 2 | grep A4:C1:38:0D:3B:10
 [2020-11-09T00:27:35] 46 A4:C1:38:0D:3B:10 GVH5177_3B10 (UUID) 88EC (Flags) 05 (Manu) 01000101029D1A64
 [2020-11-09T00:27:35] 42 A4:C1:38:0D:3B:10 4C000215494E54454C4C495F524F434B535F48575177F2FFC2 (Apple) (UUID) 494E54454C4C495F524F434B535F4857 (Major) 5177 (Minor) F2FF (RSSI) C2
 ^C* SIGINT: Caught Ctrl-C, finishing loop and quitting. *
 GoveeBTTempLogger Version 1.20201016-1 Built on: Oct 16 2020 at 12:13:28 (exiting)

While I was able to see the device data in Bluetooth advertisements, it’s got some differences. The code snippets above are cherry picked from one device each that I know how to interpret and the new device last.

The Manufacturer data in the new H5177 device doesn’t start with the same 88EC signature, but at least appears to be the same length as H5075, and one byte shorter than the data packet from the H5074.

17.1C 28% matches with (Manu) 01000101029D1664. If anyone can point me to a quick decode algorithm, I’ll update my project to support this device very quickly.

tp-link Smart Plugs with Energy Monitoring

Several years ago I picked up a TP-Link HS110 switch so that I could turn lights on and off on a schedule. It had an interesting feature of being able to monitor energy usage as well.

The HS110 has an unfortunate design that covers the second socket in a wall outlet and makes it unusable. I purchased several HS105 units over time because two can be plugged into a standard outlet with the only drawback being the extra distance the normal plug extends from the wall. The HS105 was on sale as multipack on a somewhat regular basis. The drawback of the HS105 is that it doesn’t offer energy monitoring.

I came across the HS300 power strip that offers six switched outlets plus energy monitoring for each outlet. It has a flat angled plug, allowing two devices to fit in a standard wall outlet.

Each of these devices seems to be rated at 15A (1875W) total. That should be fine, since most of the standard wall plugs they would be plugged into aren’t rated for more than that, but it’s interesting that the current handling of the largest devices is the same as the smallest.

The energy monitoring was an interesting feature, and I was hoping to get around to doing more than glancing at it from my phone occasionally. Nearly three years after my first purchase I finally got around to writing a program to do what I wanted to log the energy usage.

I’d come across https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ when I first bought the HS110, and thought I would get around to doing what I wanted quickly, but as with so many projects, it was set aside as less important. With the transient nature of the web, I’m glad that this site is still visible, and the resulting github repository tools proved invaluable for me getting my project working. https://github.com/softScheck/tplink-smartplug

There are several python projects for communicating with these devices which I also found useful, but I was hoping to build a small program with very few dependencies. Part of what I wanted to know was the communication protocol over the ethernet, and that took the most time to decipher.

https://github.com/wcbonner/KasaEnergyLogger is my project, with all of the work done in a single threaded C++ file. I’ll hopefully describe what I know of the protocol in the future. As it is, I’m pulling data from multiple devices and logging it using MRTG. I know there are significantly better graphics dashboards available, but this requires very little infrastructure, and I’m logging the raw data in case I ever really want to revisit it.

MRTG graph of AC Power Usage

For most people these devices connect to Alexa or Google Home and the scheduling plus voice controls are all that they will ever use.

I was very happy with having lamps set to turn on at sunset and turn off at specific times. The fact that I live at a latitude where sunset changes from after 9pm in the peak of summer to before 5pm midwinter was plenty for me. I also use them for controlling fans to adjust the climate in my home when I’m not relying on air conditioning.

From a system monitoring perspective I’ve considered having two Raspberry Pi, each plugged into a HS105, monitoring each other and power cycling the other device if it can’t be reached for a designated period of time.

Here are some of the other sites I found useful in getting to my current state:

GoveeBTTempLogger as a Debian Package

After getting my program to listen and log Bluetooth Low Energy advertisements from Govee thermometers running reliably, I needed to figure out how to make the program automatically start when my Raspberry was rebooted. I was led down two paths to get things working, systemd unit files, and debian package files created with dpkg-deb.

The final file structure I came up with is visible in https://github.com/wcbonner/GoveeBTTempLogger but still can use some explanation as to what I did.

To create the debian package, I created a file structure under my source repository that mimicked what I wanted to put on the target system.

\GOVEEBTTEMPLOGGER\GOVEEBTTEMPLOGGER
├───DEBIAN
│       control
│       postinst
│       postrm
│       prerm
│
├───etc
│   └───systemd
│       └───system
│               goveebttemplogger.service
│
├───usr
│   └───local
│       └───bin
│               goveebttemplogger
│
└───var
    └───log
        └───goveebttemplogger
                gvh507x.txt

I had decided I wanted my executable to be located in /usr/local/bin. It’s the file named goveebttemplogger. I wanted it to write log files into /var/log/goveebttemplogger/ and the easiest way to make sure that directory was created was to put a zero length file in that directory, gvh507x.txt.

The files in the DEBIAN directory are used by the dpkg-deb program when building the distributable package. More on those later.

To get the program configured to automatically run when the machine boots, and properly stop when it shuts down, I settled on the systemd unit files as the both the easiest and most reliable method. I’ve been around linux long enough to first think of /etc/rc.local manipulation, then script files for various runlevels in the /etc/init.d/ directories, and was amazed at both the power and ease of setting up to use the systemd unit files. The hardest part was figuring out what other services my program must have already started. I knew it was dependent on Bluetooth, but the specific services was a bit of a guess.

# Contents of /etc/systemd/system/goveebttemplogger.service
[Unit]
Description=GoveeBTTempLogger service
After=bluetooth.target dbus-org.bluez.service network.target
Requires=bluetooth.target
KillSignal=SIGINT

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/goveebttemplogger -v 0 -l /var/log/goveebttemplogger/

[Install]
WantedBy=multi-user.target

After creating that file in the specified location, I was able to issue the following commands to make systemd start the program.

sudo systemctl daemon-reload
sudo systemctl enable goveebttemplogger.service
sudo systemctl start goveebttemplogger.service

The most unique bit of my unit file is that I specifically want my program to be sent the SIGINT signal to kill it, since I will recognize that and flush the log files before exiting. The ExecStart line is the command line to run my program, which I’m also specifying the log directory as one of the parameters.

I had the systemd unit file and the initial DEBIAN/control file figured out pretty easily. I’d come across this https://linuxconfig.org/easy-way-to-create-a-debian-package-and-local-package-repository article which helped understanding the control file.

Package: GoveeBTTempLogger
Version: 1.20200725-1
Section: custom
Priority: optional
Architecture: armhf
Essential: no
Installed-Size: 95
Maintainer: wcbonner@users.noreply.github.com
Description: Listen and log Govee Thermometer Bluetooth Low Energy Advertisments
Depends: libbluetooth3

What took me a while to figure out was how to get the systemctl commands to be run after the files were put in place by the package manager. There are four script commands, which I’m using three. preinst, postinst, prerm, and postrm. Each of them is a simple script and needs to be marked executable in the file system. They are each run at various stages by the package manager, Pre-Installation, Post-Installation, Pre-Removal, and Post-Removal.

#!/bin/sh
# POSTINST script for goveebttemplogger

echo "\033[36m HI I'M A POSTINST SCRIPT `date +"%s"` \033[39m"
systemctl daemon-reload
systemctl enable goveebttemplogger.service
systemctl start goveebttemplogger.service

exit 0

After installation of my program and the systemd unit file, I reload the systemd database, enable my service, and start my service.

#!/bin/sh
# PRERM script for goveebttemplogger

echo "\033[36m HI I'M A PRERM SCRIPT `date +"%s"` \033[39m"
systemctl stop goveebttemplogger.service
systemctl disable goveebttemplogger.service

exit 0

Before removal of my program, I stop the service and disable the service.

#!/bin/sh
# POSTRM script for goveebttemplogger

echo "\033[36m HI I'M A POSTRM SCRIPT `date +"%s"` \033[39m"
systemctl daemon-reload

exit 0

After removal of my program, I reload the systemd database, to make sure it’s not got my unit file in its database any longer.

When I retrieve a copy of my code with the command git clone https://github.com/wcbonner/GoveeBTTempLogger I then have a subdirectory below the GoveeBTTempLogger that is also named GoveeBTTempLogger. That deeper directory is the structure that will be created into the package.


GoveeBTTempLogger/usr/local/bin/goveebttemplogger: goveebttemplogger.cpp
        mkdir -p GoveeBTTempLogger/usr/local/bin
        g++ -lbluetooth goveebttemplogger.cpp -o GoveeBTTempLogger/usr/local/bin/goveebttemplogger

deb: GoveeBTTempLogger/usr/local/bin/goveebttemplogger GoveeBTTempLogger/DEBIAN/control GoveeBTTempLogger/etc/systemd/system/goveebttemplogger.service
        mkdir -p GoveeBTTempLogger/var/log/goveebttemplogger
        touch GoveeBTTempLogger/var/log/goveebttemplogger/gvh507x.txt
        chmod a+x GoveeBTTempLogger/DEBIAN/postinst GoveeBTTempLogger/DEBIAN/postrm GoveeBTTempLogger/DEBIAN/prerm
        dpkg-deb --build GoveeBTTempLogger

I made the very simple makefile above to both compile the code and build the debian package with the simple command of make deb. It produces the package ‘goveebttemplogger’ in ‘GoveeBTTempLogger.deb’.

I can then install the package and start it running with the command sudo apt-get install ./GoveeBTTempLogger.deb

I can stop and either remove it or purge it with the command sudo apt-get remove goveebttemplogger or sudo apt-get purge goveebttemplogger.