It's time to wrap things up for the re-write of the legacy application (at least the immediate features). In
Part 1, we looked at the existing home automation application and came up with a minimum viable product (MVP). As a reminder, here are the features that we need:
Needed for MVP:
- Send commands through the serial dongle
This is the purpose of the system.
- Send commands for 8 devices on House Code "A"
These are the only devices that are actively used.
- Fire on/off/dim events on a schedule
To maintain the air conditioner functionality (and current lighting schedule).
In
Part 2, we built a test library to send commands through the serial port to the home automation hardware (thus fulfilling requirement #1). In
Part 3, we looked at generalizing the functionality so that we could send commands to various devices (thus fulfilling requirement #2).
All that's left is to create a scheduler. Sounds pretty simple, but I also needed to create some wrappers and test objects along the way.
[Update: Code download and links to the entire series of articles: Rewriting a Legacy App]
Concealing Details
As mentioned in Part 3, I wasn't really happy with the way that I left the objects. Here's how we interacted with the SerialCommander and MessageGenerator:
Rather than having the application need to know the details of how to generate the message and then call into the commander, we'll wrap things up into a single class that handles those details. Here's what I want my application code to look like:
So, I created a HouseController object that wraps up the message generation and serial port interaction. This has a SendCommand method that takes a device number and a command. From the HouseController class:
This method uses the static MessageGenerator class to get the message, and then it uses the a private instance of the SerialCommander object (the "Commander" property) to interact with the serial port.
Faking Hardware Interaction
As I was going through creating these objects and moving code around, I ran into a restriction that I found to be a bit of a hassle. I needed to have the serial dongle plugged into my development machine in order for the code to run. If I ran the code without the dongle plugged in, I got the following error when calling into the SerialCommander object:
Since I was comfortable that the hardware interaction was working, I wanted to be able to continue to develop even when the hardware was not plugged in to my dev machine -- specifically, I wanted to keep it plugged in to the machine running the legacy application so that it would continue to function.
In order to do this, I created a simple interface: ICommander. Since the SerialCommander only has one publicly-exposed method, this interface is pretty simple:
With this interface in place, I could add the abstraction to the HouseController class (through the Controller property) and provide a fake implementation that didn't require the hardware.
The FakeCommander implementation is pretty simple:
It doesn't need to actually do anything, but I echo the message to the console window so that I could see that the method was getting called.
Inside the HouseController class, I set up the property so that we could easily use Property Injection to swap out a fake commander:
For more information on Property Injection, check out my materials on
Dependency Injection (and specifically a blog article on
the Property Injection pattern).
If we do nothing (that is, we do not set this property directly), then the first time it is used, it will automatically create an instance of the SerialCommander object. This is the default behavior that we would like to have in our production environment.
But for testing, we can override the property with a fake or test implementation. Here's our updated application code that injects the FakeCommander:
Notice that after we create the HouseController object, but before we call any methods on it, we set the "Commander" property to our fake object. This lets us run our application without actually interacting with a serial port.
Scheduling Data
What we really need to do is implement a schedule. For that, we'll need to be able to read the data from a persistent location (and in the future, add some UI that will make it easy to manage this data). I decided to keep things pretty simple. I created a ScheduleItem object that represents the data that we need:
And I created a Schedule object which is a collection of ScheduleItems. In addition, this object is able to load data from a CSV file from the file system. I decided on a CSV file because it is fairly simple, and I already had code that I could borrow from another project.
Here's what the schedule file looks like:
And here's what the loading code looks like (in the Schedule class):
There isn't any error handling here, so if the data is bad, we'll run into problems. But we're good for the MVP. We'll work on making this more robust as we need to.
Now that we have the schedule data, we need to execute it somehow.
Executing Scheduled Commands
I added the scheduling functionality into the HouseController class. I'm just using a Timer for this (the Timer is coming from System.Timers -- this seemed to be the best for my needs):
In this code, we set up a timer that is set to fire every 30 seconds. In the constructor, we hook up the event handler and start the timer. In addition, notice the "Schedule" property. This gets instantiated when this object is constructed (and the constructor code of the Schedule loads up the data from the file -- we'll look at this in just a bit).
When the timer fires, it runs the following code:
This code looks at the schedule items and filters the list based on items that are (1) enabled and (2) are scheduled to occur within 1 minute of the current time (I'll talk about this 1 minute window in just a bit). This calculation is done in the TimeDurationFromNow helper method:
This code is dealing with "TimeOfDay" because we want to ignore the date portion of the datetime values and just look at the times. The "Duration" method will give us the absolute value of our subtraction (so we'll always end up with a positive number).
Once we have a list of items that we need to process based on the current time, the scheduled time, and whether they are enabled, we just loop through them and call the "SendCommand" method for each one.
The last step in the timer event handler is to spit out some debug code to our console. This way we can check to make sure that our filter is running correctly.
Testing the Schedule
Testing schedulers is always fun. One way is to update the data file so that the scheduled time is in the not-too-distant future, then start the application and wait. This is not a fun way of doing things, so I cheat just a little bit.
Here's the constructor of our Schedule class:
In addition to loading the schedule from the file, I manually add 3 new schedule items for the not-too-distant future. This way, I don't have to wait very long, and I don't have to constantly update the data file.
Now, if we run our console application (and wait several minutes), we see the following output:
This may look a bit strange, so let's walk through this. The first 4 lines are coming straight from the Main method that we saw earlier. It outputs "Starting Test", turns on device #5, turns off device #5, and then outputs "Test Completed". Everything after that is from the Timer event running.
The first time the timer "ticks", it processes one record (our first test record from above). Remember that the timer fires 30 seconds after our application starts, and our first schedule item is set for 1 minute after the application starts. Since this is within the 1 minute window that we have defined above, that first command is run.
Then we have the output that shows us that 1 schedule item was processed, that we have 17 total items in our schedule (which includes 14 items from the file and our 3 items in code), and that 8 of the schedule items are active (in our file, the "Summer" items are marked as inactive).
If we look at the next set of records (from 5:35), we see that 2 schedule items are processed. The first command is sent (again) since it is still within the 1 minute window, and the second is sent since it enters that 1 minute window.
And if we follow this through, we can see all 3 of our test records go through the process.
Why the 1 Minute Window?
So you've probably noticed that we end up sending each command 3 times (30 seconds apart). This is a result of having the 1 minute window set up in our scheduler. But why do we need this?
This is based on my experience with the hardware. What I've found is that not every command gets registered by the system. I don't know if this is because the commands aren't received or if there is temporary interference in the power lines that prevent the transmission to the module. I just know that it is not 100% reliable.
Because of this, in the legacy application, I set up this processing window so that schedule commands would be sent multiple times. In practice this isn't a problem. If I send a command to turn off the air conditioner and it is already off, then nothing happens. And this is better than a command getting "missed" and a device staying on longer than intended (or not coming on as intended).
I'll need to do some long-term testing to see if this is still needed. But since the problems are intermittent, there's no way that I'll really know until the code is running for a while.
This Completes the MVP
The code isn't pretty (but it's not ugly, either). And there are still a lot of features that I'd like to add. But this completes our minimum viable product -- that is, a product that we can put into production to replace the current legacy system. Let's quickly review:
Needed for MVP:
- Send commands through the serial dongle
This is the purpose of the system.
- Send commands for 8 devices on House Code "A"
These are the only devices that are actively used.
- Fire on/off/dim events on a schedule
To maintain the air conditioner functionality (and current lighting schedule).
We have fulfilled all of these requirements. We can send commands through our serial dongle; we can communicate with 8 devices on house code "A"; and we can send commands based on a persistable schedule. So that's all we need!
More To Come
But even though this is all I *need* to replace the existing application, this is not all that I want from the system. I would like to have a UI to set the schedule (and enable/disable schedule sets). I also want to code up the "dimming" commands (I deferred this for the MVP since I decided it was not critical to initial implementation).
I would also like to have a network-aware interface so that I can send commands remotely. And I would also like something that would keep a record of the state of each device. This last one is a bit difficult to handle. There is no way for us to query the system, which means that we need to maintain state information ourselves. And there's the added difficulty that if someone turns a device on or off using the physical remote, there's no way for our application to know about that.
Wrap Up
This has been an interesting exercise to look at an existing application and work through implementing a minimum viable product. What I discovered was that the "minimum" really wasn't that much compared to what existed in the legacy system. There were a lot of features that aren't needed (but were nice to have) and some features that aren't used at all.
The result is a very small set of features that could be implemented very quickly. It took just a few days to get everything running. Being able to release software so quickly leaves us with a good sense of accomplishment (a usable product) and gives us motivation to keep moving forward with other features.
Running through these types of exercises are important practice for us. It gives us better perspective when we're dealing with our business users. There are a lot of things that are wanted, but we can focus in on one or two items that are really needed and implement them quickly. This makes our users happy. And ultimately, this loops back around to my musing on
No Estimates and
Partnering with the Business.
What goes around, comes around.
Happy Coding!