Jekyll2021-12-21T16:04:34-06:00/feed.xmlColin’s brain dumpGlasgow based programmer, website designer and electronics guy.<br /><br />I write code, play with tech, and send stuff into spaceColin WaddellFridge Flight Tracker2021-11-28T00:00:00-06:002021-11-28T00:00:00-06:00/flight-tracker<h1 id="tldr">tl;dr</h1>
<p>I’ve put together a wooden box with a dot matrix screen which uses a Raspberry Pi and FlightRadar24 to let me know what aircraft are over my house. It has some big magnets on the back which let me mount it to my fridge. When there’s nothing overhead it shows the date, time and the temperature outside. It’s also quite pretty.</p>
<p><a href="/media/flight-tracker/screen-flight.jpg"><img src="/media/flight-tracker/screen-flight-thumb.jpg" alt="Finished flight tracker showing a flight" /></a></p>
<h2 id="what-why">What, why?</h2>
<p>Once the lockdown restrictions started to ease off my wife and I quickly realise the serene neighborhood we’d moved into was directly on the flight path for Glasgow airport. We’re far enough away that the windows don’t always shake but close enough to start noticing the difference in how each plane sounds. How cool would it be to say, “that sounds like a Engine Alliance GP7000, must be an Airbus A380”? Not very, but that didn’t put me off building something to tell me what aircraft are nearby.</p>
<p><a href="/media/flight-tracker/flight-path.png"><img src="/media/flight-tracker/flight-path-thumb.png" alt="Flight paths converging over my house" /></a></p>
<p>Plan A was a split-flap display like at the airport, but that was either going to be more effort or more money that I was willing to put in. Instead I’ve pieced together something far sleeker.</p>
<h1 id="the-build">The build</h1>
<h2 id="custom-box">Custom box</h2>
<p>As soon as I seen the <a href="https://shop.pimoroni.com/products/rgb-led-matrix-panel?variant=35962488650">32x64 Adafruit LED RBG Panels on Pimoroni</a> I knew exactly what I wanted the final product to look like. As close to a featureless box made from a light wood with the matrix display flush with the front, just deep enough to fit the electronics. I wanted it to float weightlessly on my fridge with a single slightly fancy looking cable running into it. I took to illustrator, made up a plan and went searching for a local carpenter to help me build what was in my head. Thanks to LinkedIn I found <a href="https://www.instagram.com/turnbulldesignengineering/">Lee Turnbull</a> who is an engineer and a fantastic joiner.</p>
<p><a href="/media/flight-tracker/Plane-Tracker-Model-Assembled.png"><img src="/media/flight-tracker/Plane-Tracker-Model-Assembled-thumb.png" alt="Concept illustration of the fully assembled flight tracker" /></a></p>
<p><a href="/media/flight-tracker/Plane-Tracker-Model-Disassembled.png"><img src="/media/flight-tracker/Plane-Tracker-Model-Disassembled-thumb.png" alt="Concept illustration of the disassembled flight tracker" /></a></p>
<p><a href="/media/flight-tracker/Plane-Tracker-Stack-Diagram.png"><img src="/media/flight-tracker/Plane-Tracker-Stack-Diagram-thumb.png" alt="Side view of how components stack inside the case" /></a></p>
<p>After sending him my plan he put together a design and with a little refinement Lee suggested going for interlocking laser cut birch. Inside the box there’s some big screws driven into the standoffs to give the screen’s magnets something to attach to. On the back of the box there are some screws with neodymium magnets built in which allow the screen to be mounted to my fridge. There’s then a bit of neoprene attached to the back of the box to prevent it from slipping once up on the fridge.</p>
<p>Once he was done the only thing standing between me and the box was the Hermes courier. After a week of lost-parcel-terror the finished product was delivered to much rejoicing and swearing about how crap Hermes are.</p>
<p><em>(Pro tip: if you’re too Scottish sounds for a poorly implemented voice-recognition system to direct you to a human, I recommend using the text-to-speech feature on your computer)</em></p>
<h2 id="electronics">Electronics</h2>
<p>Thankfully the tricky electronics required to run the screen are taken care of by another great product on Pimoroni, the <a href="https://shop.pimoroni.com/products/adafruit-rgb-matrix-bonnet-for-raspberry-pi">Adafruit RGB Matrix Bonnet for the Raspberry Pi</a>. This also cemented the decision that this project was going to be built around a Raspberry Pi. I went with a <a href="https://shop.pimoroni.com/products/raspberry-pi-zero-w?variant=39458414297171">Pi Zero W</a> to minimise how much space would be needed inside the box.</p>
<p><a href="/media/flight-tracker/internals-in-case.jpg"><img src="/media/flight-tracker/internals-in-case-thumb.jpg" alt="Finished flight tracker with screen off showing electronics" /></a></p>
<p>Power comes from a <a href="https://www.amazon.co.uk/gp/product/B01DPXDB04/">5v supply brick normally used for powering LED light strips</a>. That cable is nowhere near tasty enough for the project so the plug was been swapped out for a <a href="https://uk.farnell.com/lemo/fgg-0b-302-clad52z/plug-free-0b-2way/dp/3817222?CMP=i-ddd7-00001003">Lemo 0B push-pull connector</a> and paired with it’s <a href="https://uk.farnell.com/lemo/egg-0b-302-cll/circular-connector-rcpt-2-way/dp/3817076?CMP=i-ddd7-00001003">panel-mount partner</a>. Any excuse I’ll use these, they are gorgeous - totally the wrong kind of connector for the job, but gorgeous.</p>
<p>I’ve added a little extra pizzazz to the cable by jacketing it with some purple braiding and a little heat-shrink for strain relief.</p>
<p><a href="/media/flight-tracker/lemo-connector.jpg"><img src="/media/flight-tracker/lemo-connector-thumb.jpg" alt="Close up of Lemo connector on the cable mating with a Lemo panel connector" /></a></p>
<p>A power toggle wasn’t in the original plan but I figured an extra LED and a switch that looks like a tiny antenna wasn’t to be passed up. The switch is a <a href="https://uk.farnell.com/nkk-switches/m2112tcw01/toggle-switch-1pole-red-led/dp/1187767">NKK SPDT</a> with a little red LED at the end of it.</p>
<p><a href="/media/flight-tracker/switch.jpg"><img src="/media/flight-tracker/switch-thumb.jpg" alt="Close up of the power switch and its wiring" /></a></p>
<p>To keep everything tidy inside the box I used a small strip of Veroboard to pull together:</p>
<ul>
<li>Incoming power</li>
<li>On/Off switch</li>
<li>Raspberry Pi power input</li>
<li>LED and current limiting resistor</li>
</ul>
<p>To bring power from the panel connector to the Veroboard I used the left over length of cable from the power supply with the ferrite choke. Leaving the choke on helps to clean up the supply voltage. The remaining part of the cable with the barrel jack on it is used to hook up the Raspberry Pi to the Veroboard. So it doesn’t get fried the LED needs a little inline resistor to limit current, a back-of-an-envelop calculation and a rummage through my toolkit revealed a 330R would do the job.</p>
<p><a href="/media/flight-tracker/internals.jpg"><img src="/media/flight-tracker/internals-thumb.jpg" alt="Close up of the internal electronics on a Vero board" /></a></p>
<p>Everything is assembled using 18AWG single core prototyping wire and some terminal screw blocks. All the unused Veroboard has been isolated so that I can safely use a few layers of double-sided padded tape to mount the electronics to the back of the box. I used more of the same tape to fix the Raspberry Pi to the Veroboard.</p>
<h2 id="software">Software</h2>
<p><a href="https://github.com/ColinWaddell/its-a-plane-python">A link to the source-code.</a></p>
<p>At first I was going to use the <a href="https://github.com/hzeller/rpi-rgb-led-matrix">C++ bindings that are available on Github</a> for the display as an excuse to brush up on my C++, but then CMake quickly dissuaded me of such a silly notion. God I hate a <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> file.</p>
<p>Thankfully the fantastic people providing these drivers have also made a Python version available. I also followed their instructions to up the quality of the display by <a href="https://learn.adafruit.com/assets/57727">soldering a little bridge between two pins on their bonnet</a> so the Raspberry Pi’s sound adaptor can be used to regulate the PWM of the display circuitry.</p>
<p>With the electronics hooked up I designed a simple animation engine for this matrix display. I’m really happy with how this code came together - each element of the screen is handled by a different method in my application, these include:</p>
<ul>
<li>A flight’s source and destination</li>
<li>The flight number</li>
<li>A scroller featuring the make and model of the plan</li>
<li>Shapes and lines to help divvy-up the information</li>
<li>A flashing pixel to show when data is being requested</li>
</ul>
<p><a href="/media/flight-tracker/fridge-flight-tracker.gif"><img src="/media/flight-tracker/fridge-flight-tracker-320.gif" alt="Flight tracker displaying time, followed by a flight" /></a></p>
<p>Frame-by-frame, as the engine spins it’s told how often to each run method: some run every single frame (the scrolling text), some every second frame (pulsing loading light), sometimes only once every second (the temperatures data).</p>
<p>This is what’s going on in module <code class="language-plaintext highlighter-rouge">display</code> (<a href="https://github.com/ColinWaddell/its-a-plane-python/blob/master/display/__init__.py">link to source</a>)</p>
<p>As an example, this is what it looks like increasing and decreasing the brightness of the pixel used to indicate data is being loaded. This code is triggered on every 2nd frame (<code class="language-plaintext highlighter-rouge">@Animator.KeyFrame.add(2)</code>). The number of times this function has been called is passed in as variable <code class="language-plaintext highlighter-rouge">count</code> and if we return <code class="language-plaintext highlighter-rouge">True</code> this counter is reset. This functionality is used throughout the application to control animations.</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="o">@</span><span class="n">Animator</span><span class="p">.</span><span class="n">KeyFrame</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">loading_pulse</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">count</span><span class="p">):</span>
<span class="n">reset_count</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">overhead</span><span class="p">.</span><span class="n">processing</span><span class="p">:</span>
<span class="c1"># Calculate the brightness scaler and
</span> <span class="c1"># ensure it's within a sensible range
</span> <span class="n">brightness</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="p">(</span><span class="n">count</span> <span class="o">/</span> <span class="n">BLINKER_STEPS</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span>
<span class="n">brightness</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="p">(</span><span class="n">brightness</span> <span class="o"><</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">brightness</span> <span class="o">></span> <span class="mi">1</span><span class="p">)</span> <span class="k">else</span> <span class="n">brightness</span>
<span class="bp">self</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">SetPixel</span><span class="p">(</span>
<span class="n">BLINKER_POSITION</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="n">BLINKER_POSITION</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
<span class="n">brightness</span> <span class="o">*</span> <span class="n">BLINKER_COLOUR</span><span class="p">.</span><span class="n">red</span><span class="p">,</span>
<span class="n">brightness</span> <span class="o">*</span> <span class="n">BLINKER_COLOUR</span><span class="p">.</span><span class="n">green</span><span class="p">,</span>
<span class="n">brightness</span> <span class="o">*</span> <span class="n">BLINKER_COLOUR</span><span class="p">.</span><span class="n">blue</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># Only count 0 -> (BLINKER_STEPS - 1)
</span> <span class="n">reset_count</span> <span class="o">=</span> <span class="p">(</span><span class="n">count</span> <span class="o">==</span> <span class="p">(</span><span class="n">BLINKER_STEPS</span> <span class="o">-</span> <span class="mi">1</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># Not processing, blank the square
</span> <span class="bp">self</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">SetPixel</span><span class="p">(</span><span class="n">BLINKER_POSITION</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">BLINKER_POSITION</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="k">return</span> <span class="n">reset_count</span></code></pre></figure>
<p>Flight information is being polled using the <a href="https://pypi.org/project/flightradar24/">official FlightRadar24 python module</a>. I’ve wrapped this up in some of my own code, <code class="language-plaintext highlighter-rouge">overhead.py</code> (<a href="https://github.com/ColinWaddell/its-a-plane-python/blob/master/utilities/overhead.py">link to source</a>) to help format the data and perform some rate limiting on how many requests I’m making to FlightRadar24.</p>
<p>When there’s no flight overhead the screen displays the current date, time and the temperature outside. The temperature information comes from another of my websites, <a href="https://www.taps-aff.co.uk">taps-aff.co.uk</a>. The code for pulling this data is in <code class="language-plaintext highlighter-rouge">temperature.py</code> (<a href="https://github.com/ColinWaddell/its-a-plane-python/blob/master/scenes/temperature.py#L14">link to source</a>)</p>
<p><a href="/media/flight-tracker/screen-time.jpg"><img src="/media/flight-tracker/screen-time-thumb.jpg" alt="Finished flight tracker showing a flight" /></a></p>
<h1 id="finished-product">Finished Product</h1>
<p>I couldn’t be happier with the finished unit. Lee done an amazing job with the case and it sits happily up on the fridge without slipping.</p>
<p><a href="/media/flight-tracker/tracker-on-fridge.jpg"><img src="/media/flight-tracker/tracker-on-fridge-thumb.jpg" alt="Flight tracker sitting on the fridge" /></a></p>
<p>It took a while for everything to come together but making this exactly as I wanted, down to the colour of the internal wiring, was 100% percent worth the effort. I’m really happy to have seen this finished exactly like the daft idea which had initially popped into my head.</p>
<p>Most importantly I can now hear the difference between a Boeing and an Airbus.</p>
<h2 id="future-improvements">Future improvements</h2>
<p>Software is never finished, only abandoned. With that in mind, I’m probably going to be tweaking the animation for some time. Colours, speeds, everything is fair game.</p>
<p>For the electronics, first thing I want to do is make the LED in the switch flash. This would look much cooler than the pulsing loading pixel. To do this I’m going to need to add another resistor and a transistor to the Veroboard - that’ll need need hooked up to one of the GPIOs on the Raspberry Pi. I don’t expect to get round to this any time soon!</p>
<p><a href="/media/flight-tracker/switch-light.jpg"><img src="/media/flight-tracker/switch-light-thumb.jpg" alt="Power switch showing built in LED" /></a></p>Colin Waddelltl;dr I’ve put together a wooden box with a dot matrix screen which uses a Raspberry Pi and FlightRadar24 to let me know what aircraft are over my house. It has some big magnets on the back which let me mount it to my fridge. When there’s nothing overhead it shows the date, time and the temperature outside. It’s also quite pretty. What, why? Once the lockdown restrictions started to ease off my wife and I quickly realise the serene neighborhood we’d moved into was directly on the flight path for Glasgow airport. We’re far enough away that the windows don’t always shake but close enough to start noticing the difference in how each plane sounds. How cool would it be to say, “that sounds like a Engine Alliance GP7000, must be an Airbus A380”? Not very, but that didn’t put me off building something to tell me what aircraft are nearby. Plan A was a split-flap display like at the airport, but that was either going to be more effort or more money that I was willing to put in. Instead I’ve pieced together something far sleeker. The build Custom box As soon as I seen the 32x64 Adafruit LED RBG Panels on Pimoroni I knew exactly what I wanted the final product to look like. As close to a featureless box made from a light wood with the matrix display flush with the front, just deep enough to fit the electronics. I wanted it to float weightlessly on my fridge with a single slightly fancy looking cable running into it. I took to illustrator, made up a plan and went searching for a local carpenter to help me build what was in my head. Thanks to LinkedIn I found Lee Turnbull who is an engineer and a fantastic joiner. After sending him my plan he put together a design and with a little refinement Lee suggested going for interlocking laser cut birch. Inside the box there’s some big screws driven into the standoffs to give the screen’s magnets something to attach to. On the back of the box there are some screws with neodymium magnets built in which allow the screen to be mounted to my fridge. There’s then a bit of neoprene attached to the back of the box to prevent it from slipping once up on the fridge. Once he was done the only thing standing between me and the box was the Hermes courier. After a week of lost-parcel-terror the finished product was delivered to much rejoicing and swearing about how crap Hermes are. (Pro tip: if you’re too Scottish sounds for a poorly implemented voice-recognition system to direct you to a human, I recommend using the text-to-speech feature on your computer) Electronics Thankfully the tricky electronics required to run the screen are taken care of by another great product on Pimoroni, the Adafruit RGB Matrix Bonnet for the Raspberry Pi. This also cemented the decision that this project was going to be built around a Raspberry Pi. I went with a Pi Zero W to minimise how much space would be needed inside the box. Power comes from a 5v supply brick normally used for powering LED light strips. That cable is nowhere near tasty enough for the project so the plug was been swapped out for a Lemo 0B push-pull connector and paired with it’s panel-mount partner. Any excuse I’ll use these, they are gorgeous - totally the wrong kind of connector for the job, but gorgeous. I’ve added a little extra pizzazz to the cable by jacketing it with some purple braiding and a little heat-shrink for strain relief. A power toggle wasn’t in the original plan but I figured an extra LED and a switch that looks like a tiny antenna wasn’t to be passed up. The switch is a NKK SPDT with a little red LED at the end of it.Keychron function keys configuration2020-05-26T00:00:00-05:002020-05-26T00:00:00-05:00/keychron-function-keys-configuration<h1 id="a-list-of-shortcuts">A list of shortcuts</h1>
<p>I’ve yet to find a single list of all the key combinations that can be used to configure my new Keychron K2 keyboard so I’m dumping them all here as a reference for myself. These are all based on the v1.62 firmware.</p>
<table>
<thead>
<tr>
<th>Key Combination</th>
<th>Hold</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + I + D</code></td>
<td>6 seconds</td>
<td>Toggle between <code class="language-plaintext highlighter-rouge">INSERT</code> and <code class="language-plaintext highlighter-rouge">DELETE</code> being the primary usage of the <code class="language-plaintext highlighter-rouge">DEL</code> key</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + S + O</code></td>
<td>3 seconds</td>
<td>Toggle between Auto Sleep Mode being ON and OFF</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + CAPSLOCK + P</code></td>
<td>6 seconds</td>
<td>Toggle the backlight indicator functionality of the <code class="language-plaintext highlighter-rouge">CAPS LOCK</code> key</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + L + LAMP</code></td>
<td>6 seconds</td>
<td>Toggle the lock of the light effect you are using now</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + Z + J</code></td>
<td>5 seconds</td>
<td>Factory reset</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + LAMP</code></td>
<td>Tap</td>
<td>Toggle backlight</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + B</code></td>
<td>Tap</td>
<td>Use backlight to indicate battery level</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 1</code></td>
<td>3 seconds</td>
<td>Pair bluetooth device #1</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 2</code></td>
<td>3 seconds</td>
<td>Pair bluetooth device #2</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 3</code></td>
<td>3 seconds</td>
<td>Pair bluetooth device #3</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 1</code></td>
<td>Tap</td>
<td>Use as bluetooth device #1</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 2</code></td>
<td>Tap</td>
<td>Use as bluetooth device #2</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + 3</code></td>
<td>Tap</td>
<td>Use as bluetooth device #3</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">Fn + X + L</code></td>
<td>4 seconds</td>
<td>Toggle between <code class="language-plaintext highlighter-rouge">F1 → F12</code> being primarily function keys or multimedia keys</td>
</tr>
</tbody>
</table>Colin WaddellA list of shortcuts I’ve yet to find a single list of all the key combinations that can be used to configure my new Keychron K2 keyboard so I’m dumping them all here as a reference for myself. These are all based on the v1.62 firmware. Key Combination Hold Purpose Fn + I + D 6 seconds Toggle between INSERT and DELETE being the primary usage of the DEL key Fn + S + O 3 seconds Toggle between Auto Sleep Mode being ON and OFF Fn + CAPSLOCK + P 6 seconds Toggle the backlight indicator functionality of the CAPS LOCK key Fn + L + LAMP 6 seconds Toggle the lock of the light effect you are using now Fn + Z + J 5 seconds Factory reset Fn + LAMP Tap Toggle backlight Fn + B Tap Use backlight to indicate battery level Fn + 1 3 seconds Pair bluetooth device #1 Fn + 2 3 seconds Pair bluetooth device #2 Fn + 3 3 seconds Pair bluetooth device #3 Fn + 1 Tap Use as bluetooth device #1 Fn + 2 Tap Use as bluetooth device #2 Fn + 3 Tap Use as bluetooth device #3 Fn + X + L 4 seconds Toggle between F1 → F12 being primarily function keys or multimedia keysTaps Aff - How it got to Version 3.02019-03-08T00:00:00-06:002019-03-08T00:00:00-06:00/taps-aff-v3.0<h1 id="impetus">Impetus</h1>
<p>It’s funny how a silly idea can end up taking over your life.</p>
<p>Back in 2012 I was out <a href="https://www.instagram.com/p/BG1c_wuxem6/">walking the dog</a> down the River Kelvin in Glasgow trying to think up a simple JavaScript project. I was wanting to learn about something called <a href="https://en.wikipedia.org/wiki/Ajax_(programming)">Ajax</a> transactions. I’d need to build a website which could pull data from <em>another</em> website, do something with it, and produce a result for a user.</p>
<p>Pretty generic idea right? That kind of describes every single website-as-a-service out there, however programming is an incredibly creative persuit so as dry as my remit sounded the execution could be as weird as I wanted.</p>
<h2 id="this-is-the-result">This is the result</h2>
<p><a href="https://taps-aff.co.uk"><img src="/media/taps-aff/taps-aff-header.png" alt="Taps Aff Header" /></a></p>
<p>The site is billed as Glasgow’s premier website as it provides a single critical observation, is it Taps Oan or Taps Aff?</p>
<h1 id="nationally-endorsed-sun-stroke">Nationally endorsed sun-stroke</h1>
<p>There’s an unagreeable temperature upon which Glaswegian’s will deem it appropriate to start undressing in response to a lick of heat coming from the sky. Although no-one can settle upon when it’s appropriate (some say never) what we can agree upon is the taxonomy of the conditions: it’s Taps Aff weather.</p>
<p>Removing one’s shirt under favourable atmospheric conditions is not unique to the west-coast of Scotland, but the fervour with which locals descend upon every sun-kissed park, patio and beer garden is a source of civic pride. This <a href="https://www.telegraph.co.uk/travel/maps-and-graphics/mapped-the-sunniest-and-dullest-cities-in-europe/">could perhaps have something to do</a> with Glasgow receiving the fewest hours of sun per year than any other European city clocking in at an average total of 1202 hours. Even Reykjavik in Iceland pulls in more with 1268 hours per year and it’s a <em>bawhair</em> off being in the arctic circle.</p>
<p>This phrase Taps Aff has therefore come to embody far more than just paradoxical undressing. It represents shucking your responsibilities in favour of topping up your vitamin-D, meeting with friends under the sun and making the most of the good weather as soon as it arrives.</p>
<blockquote>
<p><strong>Taps-Aff (Scots Vernacular)</strong> Literally “tops off.” The removing of one’s shirt in the event of warm weather, a phenomenon rarely seen in Glasgow. Now an expression describing good times being had.</p>
<p><strong>Antonym:</strong> Taps-Oan, “tops on”.</p>
</blockquote>
<h1 id="so-when-is-it-taps-aff">So when is it Taps Aff?</h1>
<p>Since its inception the website has been through 3 phases, each time getting better at identifying when it’s Taps Aff:</p>
<ol>
<li>A dumb client-side JS driven version</li>
<li>Slightly smarter PHP server-side implementation</li>
<li>The current smarty-pants version implemented using Django</li>
</ol>
<h2 id="taps-aff-v10">Taps Aff v1.0</h2>
<p>At first the website would just look at the current temperature in Glasgow, including wind-chill. I very scientifically pulled the threshold of 17°C out of my arse and had the site say Taps Aff if the local temperature was greater, Taps Oan if it was lower.</p>
<p>Here’s the guts of the very first version of the site written using JQuery. Note that all the weather data I retrieved was in Fahrenheit so the actual threshold was 63°F</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20location%3D%22UKXX0061%22&format=json</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">TapsAff</span><span class="p">()</span>
<span class="p">{</span>
<span class="nx">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#dynamic-taps-message</span><span class="dl">"</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="dl">"</span><span class="s2">aff</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#dynamic-taps-message</span><span class="dl">"</span><span class="p">).</span><span class="nx">css</span><span class="p">(</span><span class="dl">"</span><span class="s2">color</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">#FF5454</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">TapsOan</span><span class="p">()</span>
<span class="p">{</span>
<span class="nx">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#dynamic-taps-message</span><span class="dl">"</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="dl">"</span><span class="s2">oan</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#dynamic-taps-message</span><span class="dl">"</span><span class="p">).</span><span class="nx">css</span><span class="p">(</span><span class="dl">"</span><span class="s2">color</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">#2A44F0</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">TapsError</span><span class="p">()</span>
<span class="p">{</span>
<span class="nx">$</span><span class="p">(</span><span class="dl">"</span><span class="s2">#dynamic-taps-message</span><span class="dl">"</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="dl">"</span><span class="s2"> error</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">ready</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="kd">var</span> <span class="nx">Result</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">getJSON</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="dl">""</span><span class="p">,</span>
<span class="kd">function</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span>
<span class="p">{</span>
<span class="nx">temp_f</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">results</span><span class="p">.</span><span class="nx">channel</span><span class="p">.</span><span class="nx">wind</span><span class="p">.</span><span class="nx">chill</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">isNaN</span><span class="p">(</span><span class="nx">temp_f</span><span class="p">))</span>
<span class="k">if</span><span class="p">(</span><span class="nx">temp_f</span><span class="o">></span><span class="mi">63</span><span class="p">)</span> <span class="nx">TapsAff</span><span class="p">();</span>
<span class="k">else</span> <span class="nx">TapsOan</span><span class="p">();</span>
<span class="k">else</span> <span class="nx">TapsError</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">TapsError</span><span class="p">);</span>
<span class="p">});</span></code></pre></figure>
<hr />
<p>By this point I figured I was done and I had learnt all I needed about Ajax and Json. That was until I started getting hundreds of visitors per day.</p>
<p>Being a coder I’m by de facto full of hubris so the thought of all that traffic on something I’d put minimal effort into gave me the heebee jeebees. I done the only logical thing: purchased the domain <a href="https://taps-aff.co.uk/">taps-aff.co.uk</a> and started working on a proper version.</p>
<h2 id="taps-aff-v20">Taps Aff v2.0</h2>
<p><a href="https://github.com/ColinWaddell/tapsaff">Version 2.0</a> of the site gave me a chance to learn more about using a <a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller">new (to me)</a> paradigm whilst structuring a project. I also had some website work using <a href="https://www.zend.com/">Zend</a> coming up so I figured learning a lighter PHP framework would be a good stepping stone so I opted to use <a href="https://github.com/erickoh/KISSMVC">KissMVC</a>.</p>
<p>Anyone who has had the pleasure of seeing one of their personal projects take off in popularity understands the praise-to-critisim ratio. By the time v2.0 of the site was flying around the internet I was getting thousands of hits per day and only the negative feedback seemed to register. This was almost always to do with the site giving its opinion on the weather and the general public disagreeing.</p>
<p><a href="/media/taps-aff/taps-aff-email.png"><img src="/media/taps-aff/taps-aff-email.png" alt="An example friendly email" /></a></p>
<h3 id="taps-aff-v21">Taps Aff v2.1</h3>
<p>By this point the site was now allowing people to search the weather anywhere in the UK so I was on the receiving end of even more <del>criticism</del> encouragement. It was important to make the site smarter so I started diving deeper into the weather data I was receiving from Yahoo.</p>
<p>Amoungst Yahoo’s current observations are a bunch of weather codes. When deciding if it was Taps Aff the site started considering these codes along with whether the sun was up. Here’s the weather codes I was using to see whether if it was definitely not Taps Aff.</p>
<figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="c1">// List of codes of non-aff weather</span>
<span class="nv">$GLOBALS</span><span class="p">[</span><span class="s1">'terrible_weather'</span><span class="p">]</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
<span class="mi">0</span><span class="p">,</span> <span class="c1">// tornado</span>
<span class="mi">1</span><span class="p">,</span> <span class="c1">// tropical storm</span>
<span class="mi">2</span><span class="p">,</span> <span class="c1">// hurricane</span>
<span class="mi">3</span><span class="p">,</span> <span class="c1">// severe thunderstorms</span>
<span class="mi">4</span><span class="p">,</span> <span class="c1">// thunderstorms</span>
<span class="mi">5</span><span class="p">,</span> <span class="c1">// mixed rain and snow</span>
<span class="mi">6</span><span class="p">,</span> <span class="c1">// mixed rain and sleet</span>
<span class="mi">7</span><span class="p">,</span> <span class="c1">// mixed snow and sleet</span>
<span class="mi">8</span><span class="p">,</span> <span class="c1">// freezing drizzle</span>
<span class="mi">9</span><span class="p">,</span> <span class="c1">// drizzle</span>
<span class="mi">10</span><span class="p">,</span> <span class="c1">// freezing rain</span>
<span class="mi">11</span><span class="p">,</span> <span class="c1">// showers</span>
<span class="mi">12</span><span class="p">,</span> <span class="c1">// showers</span>
<span class="mi">13</span><span class="p">,</span> <span class="c1">// snow flurries</span>
<span class="mi">14</span><span class="p">,</span> <span class="c1">// light snow showers</span>
<span class="mi">15</span><span class="p">,</span> <span class="c1">// blowing snow</span>
<span class="mi">16</span><span class="p">,</span> <span class="c1">// snow</span>
<span class="mi">17</span><span class="p">,</span> <span class="c1">// hail</span>
<span class="mi">18</span><span class="p">,</span> <span class="c1">// sleet</span>
<span class="mi">19</span><span class="p">,</span> <span class="c1">// dust</span>
<span class="mi">20</span><span class="p">,</span> <span class="c1">// foggy</span>
<span class="mi">21</span><span class="p">,</span> <span class="c1">// haze</span>
<span class="mi">22</span><span class="p">,</span> <span class="c1">// smoky</span>
<span class="mi">25</span><span class="p">,</span> <span class="c1">// cold</span>
<span class="mi">35</span><span class="p">,</span> <span class="c1">// mixed rain and hail</span>
<span class="mi">36</span><span class="p">,</span> <span class="c1">// hot</span>
<span class="mi">37</span><span class="p">,</span> <span class="c1">// isolated thunderstorms</span>
<span class="mi">38</span><span class="p">,</span> <span class="c1">// scattered thunderstorms</span>
<span class="mi">39</span><span class="p">,</span> <span class="c1">// scattered thunderstorms</span>
<span class="mi">40</span><span class="p">,</span> <span class="c1">// scattered showers</span>
<span class="mi">41</span><span class="p">,</span> <span class="c1">// heavy snow</span>
<span class="mi">42</span><span class="p">,</span> <span class="c1">// scattered snow showers</span>
<span class="mi">43</span><span class="p">,</span> <span class="c1">// heavy snow</span>
<span class="mi">45</span><span class="p">,</span> <span class="c1">// thundershowers</span>
<span class="mi">46</span><span class="p">,</span> <span class="c1">// snow showers</span>
<span class="mi">47</span> <span class="c1">// isolated thundershowers</span>
<span class="p">);</span></code></pre></figure>
<hr />
<p>This incarnation of the site stood for a few years and has served the most amount of traffic over the site’s life. However I was still getting beef off people for the site not being accurate enough. On top of that people were after an <a href="https://taps-aff.co.uk/api/Glasgow/">API</a>, forecasts, maps…</p>
<p>More importantly I wasn’t too happy having to keep feeding the <a href="https://en.wikipedia.org/wiki/Feature_creep">feature creature</a> in a programming language I wasn’t interested in (sorry php), using a framework that had been <a href="https://www.urbandictionary.com/define.php?term=dingied">dingied</a> by its creator 10 years ago.</p>
<p>Not going to slag Taps Aff v2.1 too much though as it’s seen <em>a lot</em> of visitors.</p>
<p><a href="/media/taps-aff/taps-aff-tracking.png"><img src="/media/taps-aff/taps-aff-tracking.png" alt="Tracking Data" /></a></p>
<h2 id="taps-aff-v30">Taps Aff v3.0</h2>
<p>In April 2018 <a href="https://github.com/ColinWaddell/TapsAff-Django/commit/282f1fbc492805bf8b214744a6f614280d8593f1">I finally conceded</a> that a complete rewrite of the site would give me the chance to <em>reinvent The Tap</em> and build something I’d be happy to keep working on.</p>
<p>Remember that time spent working on <a href="https://taps-aff.co.uk">taps-aff.co.uk</a> is meant to be educational and fun? A redesign also would give me the chance to brush up on some software development tools; my idea of a great time.</p>
<p><strong>For the nerds:</strong> I decided that for v3.0 what I’d like to play with was:</p>
<ul>
<li><a href="https://www.djangoproject.com/">Django</a> Web framework for the backend</li>
<li><a href="https://getbootstrap.com/">Bootstrap 4</a> for the frontend</li>
<li>Heavy use of <a href="https://gruntjs.com/">Grunt</a> to automate a <a href="https://github.com/ColinWaddell/TapsAff-Django/blob/master/Gruntfile.js">bunch of jobs</a></li>
<li><a href="https://jenkins.io/">Jenkins</a> for CI</li>
<li>I was tempted to use <a href="https://vuejs.org/">Vue.js</a> but in the end opted for a static site with this <a href="https://github.com/ColinWaddell/TapsAff-Django/blob/master/www/static/sass/tapsaff.scss#L107">one simple CSS trick Firefox hates</a></li>
</ul>
<p><strong>My apologies for the technobabble above, sincerely.</strong> What I meant to say was I was going to build a website <em>so fancy</em> I’d be able to mess with it’s millions of settings from my phone.</p>
<p><a href="/media/taps-aff/taps-aff-admin.png"><img src="/media/taps-aff/taps-aff-admin.png" alt="The admin interface" /></a></p>
<hr />
<p>And what about all those missing features people were after? By rebuilding the site in a more favourable framework anything’s possible. Maps? No bother. API? Piece of piss. Forecasting? Don’t sweat it.</p>
<p>In the end the rebuild and implementation of new features only took a few of weeks and was the requisite level of fun.</p>
<p><a href="/media/taps-aff/taps-aff-commits.png"><img src="/media/taps-aff/taps-aff-commits.png" alt="Workload" /></a></p>
<hr />
<p>The core of whether it’s Taps Aff <a href="https://github.com/ColinWaddell/TapsAff-Django/blob/master/www/taps/forecast.py#L54">now looks</a> a bit more professional.</p>
<p>Once weather data has been pulled from Yahoo the database considers whether the conditions are tolerable then has a look at the temperature.</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="k">def</span> <span class="nf">_test_taps_aff</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">temp_f</span><span class="p">,</span> <span class="n">daytime</span><span class="p">):</span>
<span class="s">"""
Return the current status of the weather
Arguments:
code -- Weather code returned by Yahoo API
temp_f -- Current temperature with windchill
daytime -- Is the sun is up?
"""</span>
<span class="n">taps</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'status'</span><span class="p">:</span> <span class="n">OAN</span><span class="p">,</span>
<span class="s">'message'</span><span class="p">:</span> <span class="s">""</span>
<span class="p">}</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">Weather</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">code</span><span class="o">=</span><span class="n">code</span><span class="p">,</span> <span class="n">terrible</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="ow">and</span> <span class="n">daytime</span><span class="p">:</span>
<span class="n">delta</span> <span class="o">=</span> <span class="n">Weather</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">code</span><span class="o">=</span><span class="n">code</span><span class="p">).</span><span class="n">delta</span>
<span class="n">theshold</span> <span class="o">=</span> <span class="n">CONFIG</span><span class="p">().</span><span class="n">threshold</span> <span class="o">+</span> <span class="n">delta</span>
<span class="k">if</span> <span class="n">temp_f</span> <span class="o">>=</span> <span class="n">theshold</span><span class="p">:</span>
<span class="n">taps</span><span class="p">[</span><span class="s">"status"</span><span class="p">]</span> <span class="o">=</span> <span class="n">AFF</span>
<span class="k">elif</span> <span class="n">temp_f</span> <span class="o">+</span> <span class="n">CONFIG</span><span class="p">().</span><span class="n">delta</span> <span class="o">></span> <span class="n">theshold</span><span class="p">:</span>
<span class="n">taps</span><span class="p">[</span><span class="s">"message"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"...but only by a bawhair!"</span>
<span class="k">return</span> <span class="n">taps</span></code></pre></figure>
<p><a href="https://github.com/ColinWaddell/TapsAff-Django">The source code for the current version of Taps Aff is available here</a>.</p>
<h1 id="when-is-it-taps-aff">WHEN IS IT <em>TAPS AFF</em>?</h1>
<p>If you’ve read this far you deserve to know. <strong>On the first sunny day of the year, if there’s no wind and the sky is clear the threshold sits around 17°C</strong>. The “almost there” warning comes on at about 15°C.</p>
<p>Outside of those parameters though it’s all over the place. I suppose you could reverse engineer the <a href="https://github.com/ColinWaddell/TapsAff-Django/blob/master/data/weather_codes.json">default settings</a> if you were really interested.</p>
<p>To be honest though the way the site’s currently configured I find myself wandering around tweaking the settings from my phone as I’m out and about. I suppose the final answer of when it’s Taps Aff <em>is when I decide it is</em>. What a superpower.</p>
<p><a href="/media/taps-aff/taps-aff-phone_thumb.png"><img src="/media/taps-aff/taps-aff-phone_thumb.png" alt="Phone Admin" /></a></p>
<h1 id="where-does-all-this-weather-data-come-from">Where does all this weather data come from?</h1>
<p>Since its inception I’ve been using <a href="https://developer.yahoo.com/weather/">Yahoo’s free weather service</a> to provide realtime status information along with future forecasting and location services (i.e. converting postcodes to place names).</p>
<p>Recently they updated their service and you need to have a whitelisted application in order to retrieve data from them. <a href="/yahoo-weather-example/">I’ve documented the move to this service here</a> along with any sample code you might need to do the same.</p>
<p>To keep in Yahoo’s good-books I heavily cache requests so that I don’t lose access to their free service. I’ve never made a penny off this site and don’t plan on it, so there’s no chance I could move over to a paid-for weather service. For the amount of traffic <a href="https://taps-aff.co.uk/">taps-aff.co.uk</a> receives I’d have to pay the likes of <a href="http://www.wunderground.com/">Weather Underground</a> thousands of pounds per year… although being able to do hourly forecasting would be super cool.</p>
<p><a href="/media/taps-aff/taps-aff-map_thumb.png"><img src="/media/taps-aff/taps-aff-map_thumb.png" alt="Weekly Forecast Map" /></a></p>
<h1 id="afterthoughts">Afterthoughts</h1>
<p>This website has been a really weird part of my life for ages now.</p>
<p>I’ve had job interviews where the employer has been more interesting talking about how I parse the weather outside rather than the work at hand. I was a little freaked out another time someone said “you look like that guy who made Taps Aff”.</p>
<p>There’s a whole bunch of things I’d like to get to over time. Not forgetting the personal goal for Taps Aff is to have a sandbox to play in and improve as a developer I would like to investigate:</p>
<ul>
<li>Operating the site as an <a href="https://aws.amazon.com/lambda/faqs/">AWS Lambda</a> instance with Zappa</li>
<li>Getting <a href="https://www.docker.com/">Docker</a>/<a href="https://kubernetes.io/">Kubernetes</a> involved</li>
<li>Finish the cross-platform app <a href="https://github.com/ColinWaddell/TapApp">I started developing</a> (just got push notifications to implement and it’s done!)</li>
</ul>
<p>Most of all I can’t believe this is what I’m now an expert in, it reminds me of this joke:</p>
<blockquote>
<p>A man goes into a pub in a small town and, for whatever reason, gets introduced to the clientele. There’s Farmer Jack, Barman Jim, Maurice “Dancer” and Sheepshagger John. After a few pints, the visitor’s curiosity gets the better of him and he asks John what’s with the nickname.</p>
<p>“See this pub?” asks John, “I built it, but they don’t call me Pubbuilder John? I’m the local doctor, I saved Barman Jim’s life once when he choked on a peanut, but they don’t call me Lifesaver John. Every year, I supply a huge Christmas tree for the village green, but the don’t call me Christmas Tree John.</p>
<p>“But you shag one lousy sheep…”</p>
</blockquote>
<p><a href="/media/taps-aff/taps-aff-metro.jpg"><img src="/media/taps-aff/taps-aff-metro_thumb.jpg" alt="A feature in the metro" /></a></p>
<p><a href="/media/taps-aff/taps-aff-sunday-sun.jpg"><img src="/media/taps-aff/taps-aff-sunday-sun_thumb.jpg" alt="And the Sunday Sun" /></a></p>Colin WaddellImpetus It’s funny how a silly idea can end up taking over your life. Back in 2012 I was out walking the dog down the River Kelvin in Glasgow trying to think up a simple JavaScript project. I was wanting to learn about something called Ajax transactions. I’d need to build a website which could pull data from another website, do something with it, and produce a result for a user. Pretty generic idea right? That kind of describes every single website-as-a-service out there, however programming is an incredibly creative persuit so as dry as my remit sounded the execution could be as weird as I wanted. This is the result The site is billed as Glasgow’s premier website as it provides a single critical observation, is it Taps Oan or Taps Aff? Nationally endorsed sun-stroke There’s an unagreeable temperature upon which Glaswegian’s will deem it appropriate to start undressing in response to a lick of heat coming from the sky. Although no-one can settle upon when it’s appropriate (some say never) what we can agree upon is the taxonomy of the conditions: it’s Taps Aff weather. Removing one’s shirt under favourable atmospheric conditions is not unique to the west-coast of Scotland, but the fervour with which locals descend upon every sun-kissed park, patio and beer garden is a source of civic pride. This could perhaps have something to do with Glasgow receiving the fewest hours of sun per year than any other European city clocking in at an average total of 1202 hours. Even Reykjavik in Iceland pulls in more with 1268 hours per year and it’s a bawhair off being in the arctic circle. This phrase Taps Aff has therefore come to embody far more than just paradoxical undressing. It represents shucking your responsibilities in favour of topping up your vitamin-D, meeting with friends under the sun and making the most of the good weather as soon as it arrives. Taps-Aff (Scots Vernacular) Literally “tops off.” The removing of one’s shirt in the event of warm weather, a phenomenon rarely seen in Glasgow. Now an expression describing good times being had. Antonym: Taps-Oan, “tops on”. So when is it Taps Aff? Since its inception the website has been through 3 phases, each time getting better at identifying when it’s Taps Aff: A dumb client-side JS driven version Slightly smarter PHP server-side implementation The current smarty-pants version implemented using Django Taps Aff v1.0 At first the website would just look at the current temperature in Glasgow, including wind-chill. I very scientifically pulled the threshold of 17°C out of my arse and had the site say Taps Aff if the local temperature was greater, Taps Oan if it was lower. Here’s the guts of the very first version of the site written using JQuery. Note that all the weather data I retrieved was in Fahrenheit so the actual threshold was 63°F var url = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20location%3D%22UKXX0061%22&format=json"; function TapsAff() { $("#dynamic-taps-message").html("aff"); $("#dynamic-taps-message").css("color","#FF5454"); } function TapsOan() { $("#dynamic-taps-message").html("oan"); $("#dynamic-taps-message").css("color","#2A44F0"); } function TapsError() { $("#dynamic-taps-message").html(" error"); } $(document).ready(function(){ var Result = $.getJSON(url, "", function (data) { temp_f = data.query.results.channel.wind.chill; if (!isNaN(temp_f)) if(temp_f>63) TapsAff(); else TapsOan(); else TapsError(); }) .error(TapsError); }); By this point I figured I was done and I had learnt all I needed about Ajax and Json. That was until I started getting hundreds of visitors per day. Being a coder I’m by de facto full of hubris so the thought of all that traffic on something I’d put minimal effort into gave me the heebee jeebees. I done the only logical thing: purchased the domain taps-aff.co.uk and started working on a proper version. Taps Aff v2.0 Version 2.0 of the site gave me a chance to learn more about using a new (to me) paradigm whilst structuring a project. I also had some website work using Zend coming up so I figured learning a lighter PHP framework would be a good stepping stone so I opted to use KissMVC. Anyone who has had the pleasure of seeing one of their personal projects take off in popularity understands the praise-to-critisim ratio. By the time v2.0 of the site was flying around the internet I was getting thousands of hits per day and only the negative feedback seemed to register. This was almost always to do with the site giving its opinion on the weather and the general public disagreeing. Taps Aff v2.1 By this point the site was now allowing people to search the weather anywhere in the UK so I was on the receiving end of even more criticism encouragement. It was important to make the site smarter so I started diving deeper into the weather data I was receiving from Yahoo. Amoungst Yahoo’s current observations are a bunch of weather codes. When deciding if it was Taps Aff the site started considering these codes along with whether the sun was up. Here’s the weather codes I was using to see whether if it was definitely not Taps Aff. // List of codes of non-aff weather $GLOBALS['terrible_weather'] = array( 0, // tornado 1, // tropical storm 2, // hurricane 3, // severe thunderstorms 4, // thunderstorms 5, // mixed rain and snow 6, // mixed rain and sleet 7, // mixed snow and sleet 8, // freezing drizzle 9, // drizzle 10, // freezing rain 11, // showers 12, // showers 13, // snow flurries 14, // light snow showers 15, // blowing snow 16, // snow 17, // hail 18, // sleet 19, // dust 20, // foggy 21, // haze 22, // smoky 25, // cold 35, // mixed rain and hail 36, // hot 37, // isolated thunderstorms 38, // scattered thunderstorms 39, // scattered thunderstorms 40, // scattered showers 41, // heavy snow 42, // scattered snow showers 43, // heavy snow 45, // thundershowers 46, // snow showers 47 // isolated thundershowers ); This incarnation of the site stood for a few years and has served the most amount of traffic over the site’s life. However I was still getting beef off people for the site not being accurate enough. On top of that people were after an API, forecasts, maps… More importantly I wasn’t too happy having to keep feeding the feature creature in a programming language I wasn’t interested in (sorry php), using a framework that had been dingied by its creator 10 years ago. Not going to slag Taps Aff v2.1 too much though as it’s seen a lot of visitors. Taps Aff v3.0 In April 2018 I finally conceded that a complete rewrite of the site would give me the chance to reinvent The Tap and build something I’d be happy to keep working on. Remember that time spent working on taps-aff.co.uk is meant to be educational and fun? A redesign also would give me the chance to brush up on some software development tools; my idea of a great time. For the nerds: I decided that for v3.0 what I’d like to play with was: Django Web framework for the backend Bootstrap 4 for the frontend Heavy use of Grunt to automate a bunch of jobs Jenkins for CI I was tempted to use Vue.js but in the end opted for a static site with this one simple CSS trick Firefox hates My apologies for the technobabble above, sincerely. What I meant to say was I was going to build a website so fancy I’d be able to mess with it’s millions of settings from my phone. And what about all those missing features people were after? By rebuilding the site in a more favourable framework anything’s possible. Maps? No bother. API? Piece of piss. Forecasting? Don’t sweat it. In the end the rebuild and implementation of new features only took a few of weeks and was the requisite level of fun. The core of whether it’s Taps Aff now looks a bit more professional. Once weather data has been pulled from Yahoo the database considers whether the conditions are tolerable then has a look at the temperature. def _test_taps_aff(code, temp_f, daytime): """ Return the current status of the weather Arguments: code -- Weather code returned by Yahoo API temp_f -- Current temperature with windchill daytime -- Is the sun is up? """ taps = { 'status': OAN, 'message': "" } if not Weather.objects.filter(code=code, terrible=True) and daytime: delta = Weather.objects.get(code=code).delta theshold = CONFIG().threshold + delta if temp_f >= theshold: taps["status"] = AFF elif temp_f + CONFIG().delta > theshold: taps["message"] = "...but only by a bawhair!" return taps The source code for the current version of Taps Aff is available here. WHEN IS IT TAPS AFF? If you’ve read this far you deserve to know. On the first sunny day of the year, if there’s no wind and the sky is clear the threshold sits around 17°C. The “almost there” warning comes on at about 15°C. Outside of those parameters though it’s all over the place. I suppose you could reverse engineer the default settings if you were really interested. To be honest though the way the site’s currently configured I find myself wandering around tweaking the settings from my phone as I’m out and about. I suppose the final answer of when it’s Taps Aff is when I decide it is. What a superpower. Where does all this weather data come from? Since its inception I’ve been using Yahoo’s free weather service to provide realtime status information along with future forecasting and location services (i.e. converting postcodes to place names). Recently they updated their service and you need to have a whitelisted application in order to retrieve data from them. I’ve documented the move to this service here along with any sample code you might need to do the same. To keep in Yahoo’s good-books I heavily cache requests so that I don’t lose access to their free service. I’ve never made a penny off this site and don’t plan on it, so there’s no chance I could move over to a paid-for weather service. For the amount of traffic taps-aff.co.uk receives I’d have to pay the likes of Weather Underground thousands of pounds per year… although being able to do hourly forecasting would be super cool. Afterthoughts This website has been a really weird part of my life for ages now. I’ve had job interviews where the employer has been more interesting talking about how I parse the weather outside rather than the work at hand. I was a little freaked out another time someone said “you look like that guy who made Taps Aff”. There’s a whole bunch of things I’d like to get to over time. Not forgetting the personal goal for Taps Aff is to have a sandbox to play in and improve as a developer I would like to investigate: Operating the site as an AWS Lambda instance with Zappa Getting Docker/Kubernetes involved Finish the cross-platform app I started developing (just got push notifications to implement and it’s done!) Most of all I can’t believe this is what I’m now an expert in, it reminds me of this joke: A man goes into a pub in a small town and, for whatever reason, gets introduced to the clientele. There’s Farmer Jack, Barman Jim, Maurice “Dancer” and Sheepshagger John. After a few pints, the visitor’s curiosity gets the better of him and he asks John what’s with the nickname. “See this pub?” asks John, “I built it, but they don’t call me Pubbuilder John? I’m the local doctor, I saved Barman Jim’s life once when he choked on a peanut, but they don’t call me Lifesaver John. Every year, I supply a huge Christmas tree for the village green, but the don’t call me Christmas Tree John. “But you shag one lousy sheep…”Fetch Yahoo weather data using OAuth1 authentication in Python 32019-03-06T00:00:00-06:002019-03-06T00:00:00-06:00/yahoo-weather-example<p>Recently Yahoo updated how you can access their weather API. Instead of the
previous free-for-all you now need to have a whitelisted app and authenticate
with OAuth1 when requesting data.</p>
<p>As per the warning in the <a href="https://developer.yahoo.com/weather/">documentation</a>:</p>
<blockquote>
<p>Important EOL Notice: As of Thursday, Jan. 3, 2019, the weather.yahooapis.com
and query.yahooapis.com for Yahoo Weather API will be retired.</p>
<p>To continue using our free Yahoo Weather APIs, use
https://weather-ydn-yql.media.yahoo.com/forecastrss.</p>
<p>Follow below instructions to get credentials and onboard to this free Yahoo
Weather API service.</p>
</blockquote>
<p>Yahoo have been kind enough to <a href="https://developer.yahoo.com/weather/documentation.html#oauth-python">provide a Python example</a>
showing how to request this data however it’s aimed at Python 2.7</p>
<p>Below I provide an example for Python 3.6+ (might work on previous versions, I’ve not tested)</p>
<h2 id="usage">Usage</h2>
<p>Functionality is provided via the function <code class="language-plaintext highlighter-rouge">get_yahoo_weather</code> which takes the parameters:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">location</code></li>
<li><code class="language-plaintext highlighter-rouge">app_id</code></li>
<li><code class="language-plaintext highlighter-rouge">consumer_key</code></li>
<li><code class="language-plaintext highlighter-rouge">consumer_secret</code></li>
</ul>
<h2 id="the-code">The code</h2>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">time</span><span class="p">,</span> <span class="n">uuid</span><span class="p">,</span> <span class="n">urllib</span>
<span class="kn">from</span> <span class="nn">json</span> <span class="kn">import</span> <span class="n">loads</span>
<span class="kn">import</span> <span class="nn">hmac</span><span class="p">,</span> <span class="n">hashlib</span>
<span class="kn">from</span> <span class="nn">base64</span> <span class="kn">import</span> <span class="n">b64encode</span>
<span class="k">def</span> <span class="nf">_generate_signature</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
<span class="n">key_bytes</span><span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">key</span> <span class="p">,</span> <span class="s">'utf-8'</span><span class="p">)</span>
<span class="n">data_bytes</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="s">'utf-8'</span><span class="p">)</span>
<span class="n">signature</span> <span class="o">=</span> <span class="n">hmac</span><span class="p">.</span><span class="n">new</span><span class="p">(</span>
<span class="n">key_bytes</span><span class="p">,</span>
<span class="n">data_bytes</span><span class="p">,</span>
<span class="n">hashlib</span><span class="p">.</span><span class="n">sha1</span>
<span class="p">).</span><span class="n">digest</span><span class="p">()</span>
<span class="k">return</span> <span class="n">b64encode</span><span class="p">(</span><span class="n">signature</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_yahoo_weather</span><span class="p">(</span>
<span class="n">location</span><span class="p">,</span>
<span class="n">app_id</span><span class="p">,</span>
<span class="n">consumer_key</span><span class="p">,</span>
<span class="n">consumer_secret</span><span class="p">,</span>
<span class="n">url</span><span class="o">=</span><span class="s">'https://weather-ydn-yql.media.yahoo.com/forecastrss'</span>
<span class="p">):</span>
<span class="c1"># Basic info
</span> <span class="n">method</span> <span class="o">=</span> <span class="s">'GET'</span>
<span class="n">concat</span> <span class="o">=</span> <span class="s">'&'</span>
<span class="n">query</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'location'</span><span class="p">:</span> <span class="n">location</span><span class="p">,</span>
<span class="s">'format'</span><span class="p">:</span> <span class="s">'json'</span>
<span class="p">}</span>
<span class="n">oauth</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'oauth_consumer_key'</span><span class="p">:</span> <span class="n">consumer_key</span><span class="p">,</span>
<span class="s">'oauth_nonce'</span><span class="p">:</span> <span class="n">uuid</span><span class="p">.</span><span class="n">uuid4</span><span class="p">().</span><span class="nb">hex</span><span class="p">,</span>
<span class="s">'oauth_signature_method'</span><span class="p">:</span> <span class="s">'HMAC-SHA1'</span><span class="p">,</span>
<span class="s">'oauth_timestamp'</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">())),</span>
<span class="s">'oauth_version'</span><span class="p">:</span> <span class="s">'1.0'</span>
<span class="p">}</span>
<span class="c1"># Prepare signature string (merge all params and SORT them)
</span> <span class="n">merged_params</span> <span class="o">=</span> <span class="n">query</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">merged_params</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">oauth</span><span class="p">)</span>
<span class="n">sorted_params</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">k</span> <span class="o">+</span> <span class="s">'='</span> <span class="o">+</span> <span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">quote</span><span class="p">(</span><span class="n">merged_params</span><span class="p">[</span><span class="n">k</span><span class="p">],</span> <span class="n">safe</span><span class="o">=</span><span class="s">''</span><span class="p">)</span>
<span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">merged_params</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
<span class="p">]</span>
<span class="n">signature_base_str</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">method</span> <span class="o">+</span>
<span class="n">concat</span> <span class="o">+</span>
<span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">quote</span><span class="p">(</span>
<span class="n">url</span><span class="p">,</span>
<span class="n">safe</span><span class="o">=</span><span class="s">''</span>
<span class="p">)</span> <span class="o">+</span>
<span class="n">concat</span> <span class="o">+</span>
<span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">quote</span><span class="p">(</span><span class="n">concat</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">sorted_params</span><span class="p">),</span> <span class="n">safe</span><span class="o">=</span><span class="s">''</span><span class="p">)</span>
<span class="p">)</span>
<span class="c1"># Generate signature
</span> <span class="n">composite_key</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">quote</span><span class="p">(</span>
<span class="n">consumer_secret</span><span class="p">,</span>
<span class="n">safe</span><span class="o">=</span><span class="s">''</span>
<span class="p">)</span> <span class="o">+</span> <span class="n">concat</span>
<span class="n">oauth_signature</span> <span class="o">=</span> <span class="n">_generate_signature</span><span class="p">(</span>
<span class="n">composite_key</span><span class="p">,</span>
<span class="n">signature_base_str</span>
<span class="p">)</span>
<span class="c1"># Prepare Authorization header
</span> <span class="n">oauth</span><span class="p">[</span><span class="s">'oauth_signature'</span><span class="p">]</span> <span class="o">=</span> <span class="n">oauth_signature</span>
<span class="n">auth_header</span> <span class="o">=</span> <span class="p">(</span>
<span class="s">'OAuth '</span> <span class="o">+</span>
<span class="s">', '</span><span class="p">.</span><span class="n">join</span><span class="p">(</span>
<span class="p">[</span>
<span class="s">'{}="{}"'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">k</span><span class="p">,</span><span class="n">v</span><span class="p">)</span>
<span class="k">for</span> <span class="n">k</span><span class="p">,</span><span class="n">v</span> <span class="ow">in</span> <span class="n">oauth</span><span class="p">.</span><span class="n">items</span><span class="p">()</span>
<span class="p">]</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="c1"># Send request
</span> <span class="n">url</span> <span class="o">=</span> <span class="n">url</span> <span class="o">+</span> <span class="s">'?'</span> <span class="o">+</span> <span class="n">urllib</span><span class="p">.</span><span class="n">parse</span><span class="p">.</span><span class="n">urlencode</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
<span class="n">request</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">Request</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">request</span><span class="p">.</span><span class="n">add_header</span><span class="p">(</span><span class="s">'Authorization'</span><span class="p">,</span> <span class="n">auth_header</span><span class="p">)</span>
<span class="n">request</span><span class="p">.</span><span class="n">add_header</span><span class="p">(</span><span class="s">'X-Yahoo-App-Id'</span><span class="p">,</span> <span class="n">app_id</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">request</span><span class="p">).</span><span class="n">read</span><span class="p">()</span>
<span class="k">return</span> <span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="p">)</span></code></pre></figure>Colin WaddellRecently Yahoo updated how you can access their weather API. Instead of the previous free-for-all you now need to have a whitelisted app and authenticate with OAuth1 when requesting data. As per the warning in the documentation: Important EOL Notice: As of Thursday, Jan. 3, 2019, the weather.yahooapis.com and query.yahooapis.com for Yahoo Weather API will be retired. To continue using our free Yahoo Weather APIs, use https://weather-ydn-yql.media.yahoo.com/forecastrss. Follow below instructions to get credentials and onboard to this free Yahoo Weather API service. Yahoo have been kind enough to provide a Python example showing how to request this data however it’s aimed at Python 2.7 Below I provide an example for Python 3.6+ (might work on previous versions, I’ve not tested) Usage Functionality is provided via the function get_yahoo_weather which takes the parameters: location app_id consumer_key consumer_secret The code import time, uuid, urllib from json import loads import hmac, hashlib from base64 import b64encode def _generate_signature(key, data): key_bytes= bytes(key , 'utf-8') data_bytes = bytes(data, 'utf-8') signature = hmac.new( key_bytes, data_bytes, hashlib.sha1 ).digest() return b64encode(signature).decode() def get_yahoo_weather( location, app_id, consumer_key, consumer_secret, url='https://weather-ydn-yql.media.yahoo.com/forecastrss' ): # Basic info method = 'GET' concat = '&' query = { 'location': location, 'format': 'json' } oauth = { 'oauth_consumer_key': consumer_key, 'oauth_nonce': uuid.uuid4().hex, 'oauth_signature_method': 'HMAC-SHA1', 'oauth_timestamp': str(int(time.time())), 'oauth_version': '1.0' } # Prepare signature string (merge all params and SORT them) merged_params = query.copy() merged_params.update(oauth) sorted_params = [ k + '=' + urllib.parse.quote(merged_params[k], safe='') for k in sorted(merged_params.keys()) ] signature_base_str = ( method + concat + urllib.parse.quote( url, safe='' ) + concat + urllib.parse.quote(concat.join(sorted_params), safe='') ) # Generate signature composite_key = urllib.parse.quote( consumer_secret, safe='' ) + concat oauth_signature = _generate_signature( composite_key, signature_base_str ) # Prepare Authorization header oauth['oauth_signature'] = oauth_signature auth_header = ( 'OAuth ' + ', '.join( [ '{}="{}"'.format(k,v) for k,v in oauth.items() ] ) ) # Send request url = url + '?' + urllib.parse.urlencode(query) request = urllib.request.Request(url) request.add_header('Authorization', auth_header) request.add_header('X-Yahoo-App-Id', app_id) response = urllib.request.urlopen(request).read() return loads(response)