Whole-house fans help you save energy and enjoy a more comfortable home. In the evening they can quickly cool things off after a hot day. In the morning they can cool the structure to delay (or even eliminate) the need for air conditioning as temperatures rise.

Unfortunately, getting the most out of your fan demands your attention. Accidentally running the fan when it’s too hot or cold can drive up energy bills. And when you wake up it may already be too warm to use the fan to prepare for a hot day.

That’s why I developed the Airscape whole-house fan controller. The app monitors both the fan and the weather, letting you know when it's too hot or cold to run the fan or when poor ventilation limits fan efficiency. In the morning, you can start the fan at a pre-set time to take advantage of cool outside temperatures. With the app, Airscape fan owners can get the most out of their whole-house fans.

Design Goals

Animation, color, and icons show fan context and operational status on one screen

Commonly-used controls located at the bottom of the screen for thumb reachability

Automation eliminates the need for multiple keypresses when operating fan

Integration of local weather supports timely alerts and fan operational advice

Key Features

The app automates fan control to reach the desired speed with a single keypress

Familiar wheel control  and automated fan API interaction simplifies timer integration

Notifications for high/low temps and fan interlocks as defined by the user

Automatically start the fan for a user-defined interval and at a user-defined time


Responsive, accurate performancese using Swift’s Operation class for fan API integration

iCloud key-value storage aggregates store receipts to entitle all user devices

Strings stored in one file for localization (one language currently available)

Integrated with a Swift-based server for silent notifications to support pre-cool feature

Renewable and non-renewable in-app subscription purchases with server support

Reduced View Controller complexity and enhanced maintainability

Fan and weather updates propagated to multiple consumers using observer pattern


Location information is not stored externally. Location only leaves the app when interacting with the OpenWeatherMap.org API, and the API key is not user-specific. As a consequence, it should be extremely difficult to associate location information with the user.

Personally-identifiable user information is not stored externally or internally in the app. The app's server does persistently stores a record of each subscribed fan. This record contains the app’s APNS id, the unique app UUID, and the offset from UTC (if the user sets the fan’s location). It should not be possible to extract any user-identifiable information (such as the Apple ID or the fan's specific location) from information stored on the server.

The app connects to a server hosted by Porchdog and to the weather server at openweathermap.org. The Porchdog server persistenly stores the app’s unique, vendor-specific ID, the APNS token used to initiate APNS messages, and the offset from UTC for the fan’s location (if the user allows location use by the app). This offset is used to send pre-cool reminders at appropriate times.

Weather server transactions include the fan’s GPS location (based on the phone location). No other user-sensitive information is sent to the weather server. GPS location is retrieved only when the user presses a button in the app to set or reset location, and it is not saved on the Airscape WHF server.

The Airscape WHF server never initiates outbound connections directly to the app, nor does it issue operational commands directly to the app. Instead, APNS notifications trigger the app to take action. This model permits communication with the app without the need to retain any PII. 

Two APNS-triggered actions are possible. An APNS message may direct the fan to initiate a pre-cool cycle. If so, fan operations are first validated against the user’s locally-stored settings. If local settings indicate the user has disabled pre-cool, or the notification arrives at an unexpedted time, the fan will not operate.

APNS notifications may also direct the app to provide a forecast for the following day’s high temperatures. This forecast is used to send a reminder to the user to activate pre-cool if high temperatures are in the forecast.

The Journey - Technical Challenges

Fan API Integration

The fan’s manufacturer offers “Gen 2” controls on select fans. Gen 2 fans support an API with a JSON-like interface that returns fan status and accepts basic operational commands. This API presented a number of challenges during development.

  • The API output does not conform to Codable and must be decoded with bespoke code.

  • The fan’s status reporting is finicky, especially after a command input. During fan startup, for example, the fan reports “off” even after it begins operation.

  • Limited API operational commands mimic the fan’s manual controller. Only four commands are available: speed up, slow down, turn off, and add one hour to the timer. This limitation, combined with the fan’s unreliable status reporting, made the development of reliable and responsive fan control algorithms a real challenge.

Pre-cool Feature

The pre-cool feature presented additional challenges. I wanted to 1) reliably activate the fan in the dead of night; 2) not wake the user; and 3) accomplish the task in as many app states as possible (i.e. not launched, running in the background, suspended). I evaluated three options:

  • iOS timers stop running when the app is suspended and, of course, aren’t operational before the app launches.

  • Time-of-day local iOS alerts require user acknowledgement before the app can run any code.

  • A silent remote notification - using Apple Push Notification Services - met the design criteria but required a server component. This was the solution I chose. I wrote a server using server-side Swift on the Perfect platform running on a Heroku container.

Reviewer’s Tools

Version 2 of the app is now available on the app store. You can download it and use the simulator to see how it works. I tried to make the simulator match the fan's behavior as much as possible, including the fan's finicky API. So, for example, when you first start the fan the app will not immediately respond. That's because the fan itself has not responded with a status update.

You can use the simulator as follows:

  • In the app’s “Link” tab, select “Show network address.” In the 4 available fields, enter “999.x.x.x” where x is any integer. The status text at the bottom of the page should indicate you’ve paired with the simulator.

  • Navigate to the “Fan” tab and operate the fan.

  • There are a few things to bear in mind:

  • The simulator is hosted at Heroku and is on-demand. It may require a minute or two for the server to start when you first connect. During this time the app may indicate it can’t find a fan or operations may not work.

  •  If the segment controller at the bottom of the “Fan” tab has 8 segments the simulator is running. You can also open the “Fan Detail” screen and verify the fan IP address is “fan-sim.herokuapp.com”.

  •  You can set the timer but it won’t count down. As with the real fan, you reset the timer by turning the fan off.

  • The simulator is single-threaded, meaning it is responding to changes from all simulator users. If you see erratic behavior (e.g. fan switching speeds unexpectedly) someone else may be using the simulator.

  • When using the simulator, the app entitles the pre-cool feature and allows the pre-cool time to be set in 1-minute increments. When interfacing with a real fan, I present available start times in 15 minute intervals.

  • You’ll need to change the simulator’s characteristics to see various features. I wrote a web front end to help you do that. You can find the front end here.

    If the simulator front end isn't working, fire up your favorite REST utility and send an https: GET call to fan-sim.herokuapp.com/set using the following form-encoded keys (types and ranges shown in <>, not to be included in the parameter itself). If you are not getting a status response to your REST query, the simulator is down:

    • fanSpeed <Int, 0…7>

    • damper <Int, 0…1>

    • timer <Int, 0…720>

    • interlock1 <Int, 0…1>

    • interlock2 <Int, 0…1>

    • outsideTemp <Int, any> (This simulates an optional fan temperature sensor. You can use it to test various app features. When set to a value > -90, it will override the temperature retrieved by the app from OpenWeatherMap.org.)