I'll be working on making this better. And if you have any ideas of how to do that, be sure to leave a comment.
This code is currently in a branch of the house-control repository on GitHub: jeremybytes/house-control Branch: sunset.
A Bright Idea
So this all started this morning. I was out for a walk listening to a podcast (MacBreak Weekly #437), and they were talking about home automation. Then someone mentioned something about having lights come on at sunset.
"That's brilliant," I thought. "Why didn't I think of that before?" You see, with my home automation software, one of the things that I end up doing is adjusting the schedule so that lights come on around the time it gets dark. I end up changing the schedule a few times a year (Daylight Saving Time start/end, plus adjustments every few months).
I immediately started thinking about a new feature for my software: be able to schedule items relative to sunrise and sunset. For example, I could have a light turn on at sunset or 2 hours after sunset. Or have lights that come on an hour before sunrise and turn off at sunrise. That didn't sound too difficult. I'd really need 2 things: a way to get sunrise/sunset information (hopefully from a public service), and a way to create relative schedule items.
Today, I worked on the first part: getting the sunset time.
A Sunrise / Sunset Service
After a quick search, I found a service that looked like it would work: http://sunrise-sunset.org/api. This is a fairly simple API. You just need to pass in the location in latitude and longitude along with a date, and it would return a JSON result.
I checked Google for the Lat./Long. of "Anaheim, CA", and started running some tests.
Note: The Lat./Long. is not for my house, so don't come looking for me there. I figured it would be for city hall, but it turns out to be about 2 miles east of city hall. So, I don't know how Google came up with this location.
Here's the output of the service (I took this screenshot from Fiddler just because it's an easy visualizer). This is based on the following call: http://api.sunrise-sunset.org/json?lat=33.8361&lng=-117.8897&date=2015-01-15
There's lots of information here. To start with, I really care about "sunset". But notice what time "sunset" is: 1:06:09 a.m.
The time is UTC. This wasn't a surprise because that's exactly what it said in the documentation. But now I needed to write some code that would run this service, parse the time out, and convert it to local time.
Calling the Service
I added a new project to the solution: "HouseControl.Sunset". In addition, I added an interface that has a single method (for now): "GetSunset(DateTime date)".
I created an interface because I figured that I would want to swap out the implementation in the future. In fact, I ran across some JavaScript libraries that calculate sunrise/sunset without a service call, so I might want to use that someday.
The implementation class is called "SunriseSunsetOrg" (since that's the site that we're hitting). I'll be the first to admit that I haven't done much work parsing JSON results, so I don't expect this is the best way to handle things.
Calling the service is not too difficult. I just set up a "HttpClient", call "GetAsync()", and parse the response. Here's the code to make the service call:
This creates the "HttpClient", configures the base address and asks for "JSON", formats the query string (like we saw in the sample URL above), and then makes the call with "GetAsync".
"GetAsync" is an asynchronous call (as you might have guessed). But instead of taking advantage of that, I'm asking for the "Result" property. This will stop execution of this thread until the asynchronous method is finished. This is similar to what we did with the console application when we first looked at consuming awaitable methods.
I could have made the "GetSunset" method asynchronous (and I may do that in the future), but for now, I just wanted to get valid data coming back.
Parsing the JSON Response
Now that we have the response, it's time to do something with it.
This uses "ReadAsStringAsync" to get the JSON content out of the response. Again, this is an asynchronous method, and we're just checking the "Result" property which will make this code wait until the asynchronous method is finished.
The "responseContent" variable now holds our JSON data. This is nicely parsed in the Fiddler screenshot above, but it really just looks like this:
As I mentioned, I don't work with JSON very much. So, I think there might be a better way to do this next part. I brought in the Newtonsoft JSON library with NuGet and deserialized into a dynamic object:
I used "dynamic" because I don't have a CLR type to use, and I really only need to get at a couple of the values, so it wouldn't make sense to create a custom type.
After getting the object, I check the "status" value. This is the very last value in the JSON. If it is not "OK", then I just return. We'll need some better error handling here.
Then I need to get to the "sunset" value. And since our "contentObject" is "dynamic", I can pretty much put in whatever I like. So, I just ask for the "results" object, and then check for the "sunset" value inside of that.
This would mean that "sunsetString" has a value of "1:06:09 AM".
Getting Local Time from a UTC Time String
This is where things get really ugly. Right now we have a string which represents a UTC time for sunset at our location. And we need to somehow turn this into a local date/time value.
Try not to cringe:
This is pretty scary. The first thing I do is create a DateTime object that will represent the UTC time. For this I call "DateTime.Parse" on the "sunsetString" value. Since the string only has a time, the date portion will be today's date. This will result in "Jan 15, 2015 1:06:09 AM".
Normally, when you do a DateTime.Parse, you end up with a local time (well, technically it is non-specified, but the default is to treat it as local time). This is no good since this represents UTC time. So I wrapped the DateTime.Parse in a "DateTime.SpecifyKind" call. This lets us specify that this is a "UTC" value. The result is that our "utcTime" value will be "Jan 15, 2015 1:06:09 AM (UTC)".
Since everything else in the system is in local time (at least for now), I need to change this to local time -- which is pretty easy with the "ToLocalTime" method. The result is that "localTime" will be "Jan 14, 2015 5:06:09 PM (Local)" -- and local time happens to be Pacific Standard Time (UTC -8).
Notice that the date part is a bit messed up since it now shows "yesterday" instead of "today". But this won't matter because we'll be discarding this part of the value.
To create the "resultDateTime" (which is the local sunset time that we want to return), we create a new DateTime object and pass in the date portions from our original parameter (the requested date), and the time portions from our sunset time.
The result is that we end up with "Jan 15, 2015 5:06:09 PM (Local)". And the good news is that this is the value that we're looking for.
Ugly Code
To really appreciate how ugly this code is, let's look at the entire method:
Things aren't always pretty the first time around. This code is functional, but it still needs a lot of work.
- We probably want to split out functionality into separate methods (service call, JSON parsing, date/time manipulation).
- The Lat/Long values need to be a bit more dynamic (probably coming from configuration).
- We should probably be "await"ing our asynchronous methods rather than locking the thread.
- We need better error handling.
- I'm pretty sure there's a better way to get to the "sunset" value in the JSON data.
- And the parsing of the time string... I'm not sure what it needs, but it definitely needs something.
Feel free to share your ideas in the comments for this article. Or make a pull request on the GitHub repo (I'm not quite sure how to deal with those since I'm still new to Git and GitHub, but I'll figure it out when it comes up).
[Update 1/16/2015: I know how to deal with a pull request now.]
Wrap Up
I'm not done with this code. This is just a start. I've got this on the "sunset" branch, and I won't merge this back to "master" until it's in a better state. This includes doing some clean up that I know how to do and also tapping into some friends for things I don't know how to do.
The moral of the story is that we don't always get pretty code the first time we try something. And that's okay. It's all part of the learning process. As developers, we're constantly doing things that we've never done before. It's a bit scary, at times we get get things wrong, but it's a pretty cool job to have.
Happy Coding!
No comments:
Post a Comment