<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://mikegrant.org.uk/feed.xml" rel="self" type="application/atom+xml"/><link href="https://mikegrant.org.uk/" rel="alternate" type="text/html"/><updated>2024-12-12T11:21:40+00:00</updated><id>https://mikegrant.org.uk/feed.xml</id><title type="html">Mike Grant</title><subtitle>Mike is a Software Developer interested in C#, Azure, Xamarin, MAUI and Home Automation</subtitle><author><name>Mike Grant</name></author><entry><title type="html">DevContainer CLI</title><link href="https://mikegrant.org.uk/2023/10/06/til-devcontainer-cli.html" rel="alternate" type="text/html" title="DevContainer CLI"/><published>2023-10-06T00:00:00+01:00</published><updated>2023-10-06T00:00:00+01:00</updated><id>https://mikegrant.org.uk/2023/10/06/til-devcontainer-cli</id><content type="html" xml:base="https://mikegrant.org.uk/2023/10/06/til-devcontainer-cli.html"><![CDATA[<p>For a while I’ve been getting annoyed with my workflow, when I open a project it looks like this</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code project-folder/
</code></pre></div></div> <p>Followed by waiting a few seconds, click ‘Reopen in container’, wait a few more seconds and off we go.</p> <p>Turns out, I’ve been missing a trick, a trick that goes by the name of <code class="language-plaintext highlighter-rouge">Devcontainer CLI</code></p> <p>Now there is a small gotcha with this, don’t install the npm package as described in their README. If you do that, you won’t be able to run the only feature that I really use, opening folders in a devcontainer directly from the CLI.</p> <p>Instead, using the command pallette of VS Code, select <code class="language-plaintext highlighter-rouge">&gt;Dev Containers: install devcontainer CLI</code>. This will take care of installing the CLI and map the location into your PATH..</p> <p>Now that’s out of the way, you can simply do</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>devcontainer open project-folder/
</code></pre></div></div> <p>Bingo, VS Code will open and directly start your devcontainer.</p>]]></content><author><name>Mike Grant</name></author><category term="TIL"/><summary type="html"><![CDATA[Use DevContainer CLI to simplify your life when working with devcontainers]]></summary></entry><entry><title type="html">Disabling sleep mode on Fire TV</title><link href="https://mikegrant.org.uk/2023/08/02/til-disabling-fire-tv-sleep.html" rel="alternate" type="text/html" title="Disabling sleep mode on Fire TV"/><published>2023-08-02T00:00:00+01:00</published><updated>2023-08-02T00:00:00+01:00</updated><id>https://mikegrant.org.uk/2023/08/02/til-disabling-fire-tv-sleep</id><content type="html" xml:base="https://mikegrant.org.uk/2023/08/02/til-disabling-fire-tv-sleep.html"><![CDATA[<p>A real bug bear of mine for a while has been that whilst controlling my Fire TV using ADB, the screen will occassionally go to sleep whilst I’m watching something.</p> <p>Thankfully, it’s a quick 30 second job to disable the sleep mode on the TV. You’ll need to have ADB running and connected to your Fire TV (I connect over WiFi)</p> <p>Note: If you only have one device connected to ADB, you can skip the <code class="language-plaintext highlighter-rouge">adb -s &lt;device name&gt;</code> part and just use <code class="language-plaintext highlighter-rouge">adb shell</code></p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-s</span> &lt;device name&gt; shell
settings put secure sleep_timeout 0
</code></pre></div></div> <p>That’s it, no more Fire TV going to sleep on me!</p>]]></content><author><name>Mike Grant</name></author><category term="TIL"/><summary type="html"><![CDATA[Using ADB to disable sleep mode on a Fire TV]]></summary></entry><entry><title type="html">My Personal Daily Briefing using GPT-3 and Home Assistant</title><link href="https://mikegrant.org.uk/2023/02/26/ai-personal-assistant.html" rel="alternate" type="text/html" title="My Personal Daily Briefing using GPT-3 and Home Assistant"/><published>2023-02-26T00:00:00+00:00</published><updated>2023-02-26T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2023/02/26/ai-personal-assistant</id><content type="html" xml:base="https://mikegrant.org.uk/2023/02/26/ai-personal-assistant.html"><![CDATA[<p>I built a daily personal briefing a little while ago, which gives me an overview of the day ahead and information that I’ll find relevant for my day. However, after playing around with GPT-3 for a while, I thought I could improve upon the current solution. GPT-3 should be able to summarize my briefing (as it’s currently a bit wordy and can seem repetitive day-to-day) and inject a touch of personality into it. I can still use Home Assistant to trigger an automation when I enter my office for the first time in the morning, and have it read out by the Echo in my office.</p> <p>I’m not entirely convinced that GPT-3 is the ultimate solution for this idea as there are other tools and models for text summarization that I might explore in the future. Additionally, I think it would be pretty cool to train a smaller LLM with the existing information I have using something similar to <a href="https://github.com/karpathy/nanoGPT">NanoGPT</a>.</p> <h2 id="current-daily-briefing">Current daily briefing</h2> <p>There are a few key topics that I have been using in my briefing. It currently gives me an overview of:</p> <ul> <li>Upcoming calendar events for the day</li> <li>Latest headlines (currently from Sky News)</li> <li>A simple version of the weather</li> <li>Current weather warnings (you can read more about these <a href="/2022/07/19/weather-warnings-home-assistant.html">here</a>)</li> </ul> <p>Currently the Jinja2 template for my non GPT-3 briefing looks like this.</p> <div class="language-jinja highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="cp">{%</span> <span class="k">set</span> <span class="nv">weather</span> <span class="o">=</span> <span class="nv">states</span><span class="p">(</span><span class="s1">'sensor.sidebar_weather'</span><span class="p">)</span> <span class="cp">%}</span>
<span class="cp">{%</span> <span class="k">set</span> <span class="nv">weatherWarning</span> <span class="o">=</span> <span class="s2">"Careful, there are weather warnings in effect"</span> <span class="k">if</span> <span class="nv">states</span><span class="p">(</span><span class="s1">'sensor.met_office_rss_east_midlands_weather_warnings'</span><span class="p">)</span><span class="o">|</span><span class="nf">int</span> <span class="o">&gt;</span> <span class="nv">0</span> <span class="cp">%}</span>
<span class="cp">{%</span> <span class="k">set</span> <span class="nv">weathermessage</span> <span class="o">=</span> 
<span class="p">[</span> 
    <span class="s2">"It'll be a ~weather~ day outside today, with highs of &lt;emphasis level='strong'&gt;~high~&lt;/emphasis&gt; degrees and lows of &lt;emphasis level='strong'&gt;~low~&lt;/emphasis&gt; degrees"</span><span class="p">,</span>
    <span class="s2">"It's going to be a ~weather~ day today, you can expect a high of ~high~ degrees and a low of ~low~ degrees"</span>
<span class="p">]</span><span class="o">|</span><span class="nf">random</span><span class="o">|</span><span class="nf">replace</span><span class="p">(</span><span class="s2">"~weather~"</span><span class="p">,</span><span class="nv">weather</span><span class="p">)</span><span class="o">|</span><span class="nf">replace</span><span class="p">(</span><span class="s2">"~high~"</span><span class="p">,</span><span class="nv">states</span><span class="p">(</span><span class="s1">'sensor.weather_temp_high'</span><span class="p">))</span><span class="o">|</span><span class="nf">replace</span><span class="p">(</span><span class="s2">"~low~"</span><span class="p">,</span><span class="nv">states</span><span class="p">(</span><span class="s1">'sensor.weather_temp_low'</span><span class="p">))</span>
<span class="cp">%}</span>
<span class="cp">{%</span> <span class="k">set</span> <span class="nv">headlines</span> <span class="o">=</span> <span class="nv">states.sensor.sky_news_headlines.attributes.entries</span><span class="p">[</span><span class="err">:</span><span class="nv">5</span><span class="p">]</span> <span class="o">| </span><span class="nf">map</span><span class="p">(</span><span class="nv">attribute</span><span class="o">=</span><span class="s1">'summary_detail'</span><span class="p">)</span> <span class="o">| </span><span class="nf">map</span><span class="p">(</span><span class="nv">attribute</span><span class="o">=</span><span class="s1">'value'</span><span class="p">)</span> <span class="o">| </span><span class="nf">list</span> <span class="o">| </span><span class="nf">join</span><span class="p">(</span><span class="s1">'\n'</span><span class="p">)</span> <span class="cp">%}</span>
<span class="nt">&lt;speak&gt;</span>
    Good Morning Mike!
    <span class="nt">&lt;break</span> <span class="na">strength=</span><span class="s">"x-strong"</span><span class="nt">/&gt;</span> 
    <span class="cp">{{</span> <span class="nv">weatherWarning</span> <span class="cp">}}</span>
    <span class="cp">{{</span> <span class="nv">weathermessage</span> <span class="cp">}}</span>
    <span class="nt">&lt;break</span> <span class="na">strength=</span><span class="s">"x-strong"</span><span class="nt">/&gt;</span>
    The latest headlines today: <span class="nt">&lt;break</span> <span class="na">strength=</span><span class="s">"x-strong"</span><span class="nt">/&gt;</span> <span class="cp">{{</span> <span class="nv">headlines</span> <span class="cp">}}</span>
<span class="nt">&lt;/speak&gt;</span>

</code></pre></div></div> <h2 id="creating-a-prompt-for-gpt-3">Creating a prompt for GPT-3</h2> <p>The replacement prompt that I’ve been playing has some simple instructions, a sentence limit, instructions to be concise and humorous. As well as to use <a href="https://developer.amazon.com/en-US/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html">SSML</a> to try and inject a little bit of personality into the update. You can see an example of the full prompt below.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Acting as a personal assistant, summarize the following information in an concise and humorous manner. Try to make sure that the information fits in 8 sentences. Give the response using SSML as well, to make the update upbeat.


- It's 17:49
- Currently 3.9°C outside, high of 5.0°C and a low of 0.6°C 
- Latest headlines: Rishi Sunak will meet Ursula von der Leyen, the European Commission president, tomorrow to solve a "range of complex challenges around the Protocol on Ireland and Northern Ireland".
At least 59 migrants, including 12 children, have drowned after their boat broke up off the southern coast of Italy's mainland.
Israel and the Palestinians have agreed to de-escalate tensions and "prevent more violence".
The body of missing hillwalker Kyle Sambrook and his dog have been found in Glencoe, police confirm.
A beach in Norfolk has been closed off from the public due to erosion and the risk of homes falling into the sea, the Hemsby Lifeboat crew has said.
- Calendar for today
</code></pre></div></div> <p>A sample of the output from this prompt is as follows:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;speak&gt;It's &lt;say-as interpret-as="time"&gt;17:49&lt;/say-as&gt;, and it's currently &lt;say-as interpret-as="temperature"&gt;3.9&lt;/say-as&gt; degrees outside! The high for today was &lt;say-as interpret-as="temperature"&gt;5.0&lt;/say-as&gt; and the low was &lt;say-as interpret-as="temperature"&gt;0.6&lt;/say-as&gt;. In news, Rishi Sunak is meeting Ursula von der Leyen tomorrow to solve a "range of complex challenges" concerning Ireland and Northern Ireland. There was a tragic incident in Italy, with 59 migrants, including 12 children, drowning after their boat broke up. Israel and the Palestinians have agreed to de-escalate tensions. The body of Kyle Sambrook and his dog were found in Glencoe and a beach in Norfolk has been closed off due to erosion. Don't forget about today's calendar!&lt;/speak&gt;
</code></pre></div></div> <p>Great, now that I’ve got a prompt that I’m happy with (it’s not perfect, the SSML is sporadic and inconsistent. I’m not sure how much value it adds in hindsight), it’s time to think about how this integrates with Home Assistant. If you’re using Node-Red for your automations you might want to build this a different way. But I don’t use Node-Red so, I’m planing on building a small NodeJS application. That will take advantage of being able to use the template <a href="https://developers.home-assistant.io/docs/api/rest/">API</a> that Home Assistant expose, this will allow me to build the prompt with the relevant information that all is all stored within Home Assistant. Feed this data into GPT-3 and then output the response via the Amazon Echo in my office.</p> <h2 id="adding-to-home-assistant">Adding to Home Assistant</h2> <p>With my small NodeJS app, I’m going to keep things simple. I need to call the Home Assistant API to render the template (which will form the prompt), send the prompt to GPT-3 to get the completion back and then call the notify service using the Home Assistant API to get the message read out on the Amazon Echo in my office.</p> <p>You can find the code for the entire application <a href="https://github.com/mike-grant/openai-assistant-daily-briefing">here</a></p> <p>Now that I have a small API up and running, I just need to add a Rest Command to my Home Assistant configuration that calls my API and triggers the process.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">rest_command</span><span class="pi">:</span>
  <span class="na">openai_daily_briefing</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">&lt;&lt;URL</span><span class="nv"> </span><span class="s">OF</span><span class="nv"> </span><span class="s">YOUR</span><span class="nv"> </span><span class="s">RUNNING</span><span class="nv"> </span><span class="s">NODEJS</span><span class="nv"> </span><span class="s">APP&gt;&gt;"</span>
</code></pre></div></div> <p>Now that I’ve got everything up and running to allow Home Assistant to call the API, and the API to handle the GPT-3 interaction and call back to Home Assistant to trigger the notify service, I can start building the automation. There are a few conditions that I need for this to work properly. Firstly, the briefing should only happen once per day. Second, it should only be played when I enter my office to start work in the morning. Fortunately, I can use my BLE presence detection to detect when I first enter my office each day.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="na">alias</span><span class="pi">:</span> <span class="s">Mike's Office - Briefing</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">trigger</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s">state</span>
    <span class="na">entity_id</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">sensor.mikes_phone_ble_room_presence</span>
    <span class="na">to</span><span class="pi">:</span> <span class="s">mikes-office</span>
<span class="na">condition</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">template</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">{{</span>
      <span class="s">as_timestamp(state_attr('automation.mike_s_office_briefing','last_triggered'))|timestamp_custom('%-d')</span>
      <span class="s">!= as_timestamp(now())|timestamp_custom('%-d') }}</span>
<span class="na">action</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">service</span><span class="pi">:</span> <span class="s">rest_command.openai_daily_briefing</span>
    <span class="na">data</span><span class="pi">:</span> <span class="pi">{}</span>
<span class="na">mode</span><span class="pi">:</span> <span class="s">single</span>

</code></pre></div></div>]]></content><author><name>Mike Grant</name></author><category term="Home Automation"/><category term="AI"/><summary type="html"><![CDATA[Building a personal daily briefing using GPT-3, Home Assistant and Amazon Echos to make sure I start the day with all the information I need.]]></summary></entry><entry><title type="html">Stay Ahead of Hay Fever with Home Assistant and Public Pollen Data</title><link href="https://mikegrant.org.uk/2023/02/21/uk-pollen-alerts.html" rel="alternate" type="text/html" title="Stay Ahead of Hay Fever with Home Assistant and Public Pollen Data"/><published>2023-02-21T00:00:00+00:00</published><updated>2023-07-11T00:00:00+01:00</updated><id>https://mikegrant.org.uk/2023/02/21/uk-pollen-alerts</id><content type="html" xml:base="https://mikegrant.org.uk/2023/02/21/uk-pollen-alerts.html"><![CDATA[<p>Unfortunately, I’m one of the 1 in 4<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> people who suffer from hay fever as an adult, and this weekend gave me my first reminder that I needed to start taking my antihistamines. What annoyed me (other than the runny nose and slight burning in my eyes) was that, once again, I was unprepared for it. I already know that my issue is mainly tree pollen, and pollen season for trees starts earlier than what most people associate with ‘hay fever time.’ So I turned to Home Assistant, using a scraper with Kleenex’s data, and built a system to notify me every morning if the pollen level is above a certain value.</p> <p>For my first attempt at building this system, I had planned to use the Tomorrow.io API (<a href="https://community.home-assistant.io/t/uk-and-worldwide-pollen-count-using-tomorrow-io-api/306560">HA Forum Post</a>). Unfortunately, the API returned 0 for all the values, so I needed to find a new source for the data. This could mean that the API is either inaccurate or doesn’t support the UK.</p> <p><img src="/assets/2023-02-21/tomorrow.webp" alt="Rubbish data from Tomorrow.io" class="align-center"/></p> <p>After searching the internet a bit more, I found that Kleenex offers a <a href="https://www.kleenex.co.uk/pollen-count">pollen count</a>. Thankfully, the results from their site were much more promising.</p> <p><img src="/assets/2023-02-21/kleenex.webp" alt="Kleenex pollen overview" class="align-center"/></p> <p>Now that we have a source for the data, we can use Home Assistant’s built in <a href="https://www.home-assistant.io/integrations/scrape/">scrape</a> sensor to retrieve the page and extract the relevant parts of the data to different sensors. Fortunately, I stumbled upon a helpful Home Assistant forum post that was similar to what I needed (<a href="https://community.home-assistant.io/t/help-request-pollen-sensor-dutch-hooikoortsradar-nl/524633/5">HA Forum Post</a>)</p> <p class="notice--warning"><strong>Updated 11/07/2023</strong> I noticed that a lot of the calls to the Kleenex service were failing, I did some investigation and they’ve updated their site. Thankfully, I didn’t have to do a lot to update the scrape sensor below. If you’ve followed this post before, you’ll need to update the <strong>resource</strong> and <strong>payload</strong></p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="na">scrape</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">resource</span><span class="pi">:</span> <span class="s">https://www.kleenex.co.uk/api/sitecore/Pollen/GetPollenContentCountryCity</span>
    <span class="na">method</span><span class="pi">:</span> <span class="s">POST</span>
    <span class="na">payload</span><span class="pi">:</span> <span class="s2">"</span><span class="s">city=&lt;POSTCODE&gt;&amp;country=uk</span>
    <span class="s">headers:</span>
      <span class="s">Content-Type:</span><span class="nv"> </span><span class="s">application/x-www-form-urlencoded;</span><span class="nv"> </span><span class="s">charset=UTF-8</span>
    <span class="s">scan_interval:</span><span class="nv"> </span><span class="s">3600</span>
    <span class="s">sensor:</span>
      <span class="s">-</span><span class="nv"> </span><span class="s">name:</span><span class="nv"> </span><span class="s">Pollen</span><span class="nv"> </span><span class="s">(grass)</span>
        <span class="s">select:</span><span class="nv"> </span><span class="s">"li.day-container:nth-child(1) &gt; button"</span>
        <span class="s">unit_of_measurement</span><span class="err">:</span> <span class="s2">"</span><span class="s">PPM"</span>
        <span class="na">state_class</span><span class="pi">:</span> <span class="s">measurement</span>
        <span class="na">attribute</span><span class="pi">:</span> <span class="s2">"</span><span class="s">data-grass-count"</span>
        <span class="na">unique_id</span><span class="pi">:</span> <span class="s">pollen_grass</span>
        <span class="na">value_template</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">value.split(" ")</span><span class="pi">[</span><span class="nv">0</span><span class="pi">]</span> <span class="pi">}}</span>
    <span class="err">  </span><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Pollen (weeds)</span>
        <span class="na">select</span><span class="pi">:</span> <span class="s2">"</span><span class="s">li.day-container:nth-child(1)</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">button"</span>
        <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">PPM"</span>
        <span class="na">state_class</span><span class="pi">:</span> <span class="s">measurement</span>
        <span class="na">attribute</span><span class="pi">:</span> <span class="s2">"</span><span class="s">data-weeds-count"</span>
        <span class="na">unique_id</span><span class="pi">:</span> <span class="s">pollen_weeds</span>
        <span class="na">value_template</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">value.split(" ")</span><span class="pi">[</span><span class="nv">0</span><span class="pi">]</span> <span class="pi">}}</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Pollen (trees)</span>
        <span class="na">select</span><span class="pi">:</span> <span class="s2">"</span><span class="s">li.day-container:nth-child(1)</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">button"</span>
        <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">PPM"</span>
        <span class="na">state_class</span><span class="pi">:</span> <span class="s">measurement</span>
        <span class="na">attribute</span><span class="pi">:</span> <span class="s2">"</span><span class="s">data-trees-count"</span>
        <span class="na">unique_id</span><span class="pi">:</span> <span class="s">pollen_trees</span>
        <span class="na">value_template</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">value.split(" ")</span><span class="pi">[</span><span class="nv">0</span><span class="pi">]</span> <span class="pi">}}</span>

</code></pre></div></div> <p>For my particular needs, I know that tree pollen is my arch-nemisis. So I need to build an automation that will send an alert to my phone in the morning when tree pollen levels are over <strong><em>x</em></strong>. I’ll have to tweak this value depending on how I’m feeling in the day compared to the alert that I receive.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">alias</span><span class="pi">:</span> <span class="s">Mike - Pollen Alert</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">trigger</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s">time</span>
    <span class="na">at</span><span class="pi">:</span> <span class="s2">"</span><span class="s">07:00:00"</span>
<span class="na">condition</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">numeric_state</span>
    <span class="na">entity_id</span><span class="pi">:</span> <span class="s">sensor.pollen_trees</span>
    <span class="na">above</span><span class="pi">:</span> <span class="m">100</span>
<span class="na">action</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">service</span><span class="pi">:</span> <span class="s">notify.mobile_app_pixel_4_xl</span>
    <span class="na">data</span><span class="pi">:</span>
      <span class="na">title</span><span class="pi">:</span> <span class="s">Pollen Alert</span>
      <span class="na">message</span><span class="pi">:</span> <span class="s">Don't forget to take your antihistamine today, pollen levels look ugly!</span>
<span class="na">mode</span><span class="pi">:</span> <span class="s">single</span>
</code></pre></div></div> <p><em>Hopefully this will be enough to remind me to take my antihistamines</em></p> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1" role="doc-endnote"> <p><a href="https://www.metoffice.gov.uk/weather/warnings-and-advice/seasonal-advice/health-wellbeing/pollen/surviving-hay-fever">https://www.metoffice.gov.uk/weather/warnings-and-advice/seasonal-advice/health-wellbeing/pollen/surviving-hay-fever</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>]]></content><author><name>Mike Grant</name></author><category term="Home Assistant"/><category term="Home Automation"/><summary type="html"><![CDATA[Building a system to alert me every morning if pollen levels are high using Home Assistant]]></summary></entry><entry><title type="html">Finding Dev Containers With Docker CLI</title><link href="https://mikegrant.org.uk/2023/02/10/til-finding-dev-containers.html" rel="alternate" type="text/html" title="Finding Dev Containers With Docker CLI"/><published>2023-02-10T00:00:00+00:00</published><updated>2023-02-10T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2023/02/10/til-finding-dev-containers</id><content type="html" xml:base="https://mikegrant.org.uk/2023/02/10/til-finding-dev-containers.html"><![CDATA[<p>I’ve noticed lately when working with Dev Containers a lot that most of the time that they’re not stopping the container when I close VS Code.</p> <p>Here’s a handy snippet to find all the Dev Containers on your machine using the Docker CLI</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps <span class="nt">-a</span> <span class="nt">--filter</span><span class="o">=</span><span class="s2">"label=dev.containers.id"</span> <span class="nt">--format</span> <span class="s2">"table {{.ID}}</span><span class="se">\t</span><span class="s2">{{.Status}}</span><span class="se">\t</span><span class="s2">{{.Image}}</span><span class="se">\t</span><span class="s2">{{.Label </span><span class="se">\"</span><span class="s2">devcontainer.local_folder</span><span class="se">\"</span><span class="s2">}}"</span>
</code></pre></div></div>]]></content><author><name>Mike Grant</name></author><category term="TIL"/><summary type="html"><![CDATA[Finding dev containers using Docker CLI]]></summary></entry><entry><title type="html">Controlling Streaming Services with Alexa - Part 2</title><link href="https://mikegrant.org.uk/2023/01/31/controlling-streaming-services-with-alexa-2.html" rel="alternate" type="text/html" title="Controlling Streaming Services with Alexa - Part 2"/><published>2023-01-31T00:00:00+00:00</published><updated>2023-01-31T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2023/01/31/controlling-streaming-services-with-alexa-2</id><content type="html" xml:base="https://mikegrant.org.uk/2023/01/31/controlling-streaming-services-with-alexa-2.html"><![CDATA[<p>This post is all about building an Alexa Video Skill, handling the requests and expanding the PoC created <a href="/2021/08/13/controlling-streaming-services-with-alexa-1.html">previously</a> to control your streaming services on <strong>any</strong> Android based TV device. I’ve been testing this skill out at home for a while now (my poor partner), and it’s simplified how we watch TV at home significantly. It got it’s first big test over the Christmas period when we wanted to watch some of our favourite Christmas movies, but couldn’t be bothered to check which streaming service they were on or search for them for them in each streaming service app.</p> <p>This post ended up being a lot longer than I expected when I sat down to write the final part (Maybe I should have broke it down again) but if you want to see the code behind this and skip my ramblings! It’s available on <a href="https://github.com/mike-grant/tvtuner-alexa">GitHub</a>.</p> <p><img src="/assets/2023-01-31/meme.webp" alt="All your streaming content belong to us" class="align-center"/></p> <h2 id="overview">Overview</h2> <p>There are quite a few moving parts in this project, data being sent between services and devices being controlled. Below is a high level overview of how this project and how it’s dependencies function.</p> <pre><code class="language-mermaid">sequenceDiagram;
    Alexa -&gt;&gt; Lambda: Alexa Request
    Lambda -&gt;&gt; Self Hosted Service: Forward Alexa Request
    Self Hosted Service -&gt;&gt; Trakt: Look up IMDB ID
    Self Hosted Service -&gt;&gt; JustWatch: Look up content with details from Trakt
    JustWatch -&gt;&gt; Self Hosted Service: Convert content links to deeplinks
    Self Hosted Service -&gt;&gt; ADB Server: Connect to the device and process deeplink
    ADB Server -&gt;&gt; Android TV: Send deeplink to device
</code></pre> <h2 id="creating-the-alexa-skill">Creating the Alexa skill</h2> <p>The most obvious place to start in a blog post titled ‘Controlling streaming services with Alexa’ is with the Alexa skill itself. Alexa provides several prebuilt models that you can use, which makes it a lot easier for us as it provides an extensive base set of entities and intents. To get started, head to the <a href="https://developer.amazon.com/alexa/console/ask">Alexa Developer Console</a> and select <code class="language-plaintext highlighter-rouge">Create Skill</code>.</p> <p>To begin with, we need to provide some basic details about the skill. Start by giving the skill a sensible name (I used TV Tuner) and select the <code class="language-plaintext highlighter-rouge">Primary Locale</code> that is relevant for your language, which for me is <code class="language-plaintext highlighter-rouge">English (UK)</code></p> <p>We’re now asked a few questions about what the purpose the skill is, on this screen you’re able to select one of the pre-built models that Amazon provide. Select <code class="language-plaintext highlighter-rouge">Movies &amp; TV</code> and choose the recommend model of <code class="language-plaintext highlighter-rouge">Video</code>.</p> <p><img src="/assets/2023-01-31/alexa-new-skill.webp" alt="Create a new video skill in the Alexa Developer Console" class="align-center"/></p> <p>This will automatically select the <code class="language-plaintext highlighter-rouge">Provision your own</code> option for hosting your Alexa skill. At the top of the page, click <code class="language-plaintext highlighter-rouge">Next</code>. At this point you’ll be asked to review the information provided. At the top of the page click <code class="language-plaintext highlighter-rouge">Create Skill</code>. And that’s it, the Alexa skill has been created (the joy of Amazon’s pre-built skills).</p> <h2 id="configuring-the-alexa-skill">Configuring the Alexa Skill</h2> <p>Unfortunately, because of a restriction on Amazon’s part (I’m assuming this is somehow related to using a pre-built model??). We’re required to create a Lambda function (Amazon’s brand of Serverless functions) to handle the request from Alexa. Thankfully, I had already written a proxy for handling requests from Alexa to a self hosted service for another reason (controlling my local Home Assistant). That proxy is called <a href="https://github.com/mike-grant/haaska">Haaska</a>. To save some time, I’ve modified Haaska slightly to remove the HomeAssistant specific elements and replace it with a call to the service that I had built, deployed the modified code and now the gap from Alexa to our home network (Which is where we’ll be hosting all the logic and connections for our skill) has been bridged. <em>P.S. If you’re a Home Assistant user, I’d recommend checking out <a href="https://github.com/mike-grant/haaska">Haaska</a>.</em></p> <p>You’ll need to deploy the function to AWS, and complete the initial configuration. If you’re not confident with this, you can follow most of the <a href="https://github.com/mike-grant/haaska/wiki/Setting-up-haaska#set-up-login-with-amazon">setup instructions</a> for Haaska to achieve this.</p> <h2 id="handling-the-requests-from-alexa">Handling the requests from Alexa</h2> <p>Phew, it feels like we’ve done a lot of work already and we’ve not even written a single line of code 😞 (or got any content playing on the TV). It’s time to change that. Let’s set up a new NodeJS project and get on with it! (Note, this code could probably be improved heavily by making use of the Alexa Skills Kit but this is just me hacking away on a personal project). To start with, we need to let Alexa know about the devices that can be controlled. In Alexa terms, this is part of the <code class="language-plaintext highlighter-rouge">Discovery</code> process. The same process that’s kicked off when you get a new smart light and have to tell Alexa <code class="language-plaintext highlighter-rouge">Discover new devices</code>. As this is a small project I’ve just gone ahead and hard coded my devices. Because let’s be honest, I’m not getting a new TV every day!</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nf">express</span><span class="p">();</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nf">json</span><span class="p">());</span>

<span class="kd">const</span> <span class="nx">devices</span> <span class="o">=</span> <span class="p">[{</span>
  <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">192.168.1.1</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">friendlyName</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Office TV</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">firetv</span><span class="dl">"</span>
<span class="p">}]</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="nf">async </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">directive</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">directive</span><span class="p">;</span>
  <span class="nf">if </span><span class="p">(</span><span class="nx">directive</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">directive</span><span class="p">.</span><span class="nx">header</span><span class="p">.</span><span class="nx">name</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">Discover</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Discover Request received from Alexa</span><span class="dl">'</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">alexaEndpoints</span> <span class="o">=</span> <span class="nx">devices</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="nx">x</span> <span class="o">=&gt;</span> <span class="p">({</span>
        <span class="na">displayCategories</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">TV</span><span class="dl">'</span><span class="p">],</span>
        <span class="na">capabilities</span><span class="p">:</span> <span class="p">[</span>
          <span class="p">{</span>
            <span class="na">interface</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Alexa.RemoteVideoPlayer</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">AlexaInterface</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">version</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1.0</span><span class="dl">'</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">],</span>
        <span class="na">endpointId</span><span class="p">:</span> <span class="s2">`tvtuner#</span><span class="p">${</span><span class="nx">x</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="na">description</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">x</span><span class="p">.</span><span class="nx">friendlyName</span><span class="p">}</span><span class="s2"> by TVTuner`</span><span class="p">,</span>
        <span class="na">friendlyName</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">x</span><span class="p">.</span><span class="nx">friendlyName</span><span class="p">}</span><span class="s2"> TVTuner`</span><span class="p">,</span>
        <span class="na">manufacturerName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">tvtuner</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">}));</span>
      <span class="kd">const</span> <span class="nx">resp</span> <span class="o">=</span> <span class="p">{</span>
        <span class="na">event</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">header</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">messageId</span><span class="p">:</span> <span class="nx">directive</span><span class="p">.</span><span class="nx">header</span><span class="p">.</span><span class="nx">messageId</span><span class="p">,</span>
            <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Discover.Response</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">namespace</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Alexa.Discovery</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">payloadVersion</span><span class="p">:</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span><span class="p">,</span>
          <span class="p">},</span>
          <span class="na">payload</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">endpoints</span><span class="p">:</span> <span class="nx">alexaEndpoints</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">},</span>
      <span class="p">};</span>
      <span class="nx">res</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="nx">resp</span><span class="p">);</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">sendStatus</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">3000</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nf">listen</span><span class="p">(</span><span class="nx">port</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`API server listening on port </span><span class="p">${</span><span class="nx">port</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div> <p>It feels like there’s a lot of code to achieve such a simple task, but the main points of interest are the mapping of our ‘devices’ to the Alexa discover schema, and returning that data. I’m being lazy and using the device ID as <code class="language-plaintext highlighter-rouge">tvtuner#...</code> so that I can parse the data after the <code class="language-plaintext highlighter-rouge">#</code> to know which device to play the content on.</p> <h3 id="playing-the-requested-content">Playing the requested content</h3> <p>Great, at this point Alexa knows about the devices that are capable of playing content. We now need to handle the request to actually play some content now. We get a Directive from Alexa <code class="language-plaintext highlighter-rouge">SearchAndPlay</code> (similar to the <code class="language-plaintext highlighter-rouge">Discover</code> directive above.) so we can handle it from there. I’m stripping the target down to just the IP address of the device and sending that to the new method <code class="language-plaintext highlighter-rouge">playContent</code></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">if </span><span class="p">(</span><span class="nx">directive</span><span class="p">.</span><span class="nx">header</span><span class="p">.</span><span class="nx">name</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">SearchAndPlay</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">imdb</span> <span class="o">=</span> <span class="nx">directive</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">entities</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">externalIds</span><span class="p">.</span><span class="nx">imdb</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">directive</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">.</span><span class="nx">endpointId</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">tvtuner#</span><span class="dl">'</span><span class="p">,</span> <span class="dl">''</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nf">playContent</span><span class="p">(</span><span class="nx">imdb</span><span class="p">,</span> <span class="nx">target</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">resp</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">event</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">endpoint</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">scope</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">DirectedUserId</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">directedUserId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">some-Amazon-user-id</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="na">endpointId</span><span class="p">:</span> <span class="nx">directive</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">.</span><span class="nx">endpointId</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">header</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">messageId</span><span class="p">:</span> <span class="nx">directive</span><span class="p">.</span><span class="nx">header</span><span class="p">.</span><span class="nx">messageId</span><span class="p">,</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Response</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">namespace</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Alexa</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">payloadVersion</span><span class="p">:</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">payload</span><span class="p">:</span> <span class="p">{},</span>
    <span class="p">},</span>
  <span class="p">};</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="nx">resp</span><span class="p">);</span>
  <span class="k">await</span> <span class="nx">data</span><span class="p">;</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">playContent</code> will be the method responsible for handling translating the IMDB ID, searching JustWatch and processing the deeplink.</p> <h3 id="identifying-what-we-want-to-play">Identifying what we want to play</h3> <p>We’re slowly inching closer to the end goal, Alexa knows about our devices, and now Alexa can now tell our service to play some content somewhere. We’re now getting to the first hurdle. Alexa only provides us with the IMDB id for the content that we have requested to play. But <code class="language-plaintext highlighter-rouge">JustWatch</code> doesn’t support searching with that ID, only the title of the content. We could (in theory) just use the IMDB API to get the right data back. But I’m going to use a service called <a href="https://trakt.tv">Trakt</a> (Mainly because I’m already an avid user… and a big fan of their service and <a href="https://trakt.docs.apiary.io/">API</a>). The main reason for using Trakt, is I’ll be able to filter out previously watched content, which can be handy if a TV show moves streaming services inbetween seasons. Or one service only has the latest season, but not the first x seasons.</p> <h3 id="finding-the-streaming-service">Finding the streaming service</h3> <p class="notice--warning">I’ve been kindly asked by the people over at <a href="https://www.justwatch.com/">JustWatch</a> to include a disclaimer regarding the <strong>unofficial</strong> and <strong>unsupported</strong> usage of their API.</p> <blockquote> <p>The work of many developers went and is still going into the development and maintenance of the data and the API. JustWatch’s main business is to operate a <a href="https://www.justwatch.com/">streaming guide</a> with apps for iOS and Android. They offer the data for business intelligence and marketing. Therefore it is prohibited to use the API for commercial use (consumer service, data science, business intelligence, etc.). It is ok to use the API for private projects, but please be respectful with your API calls to not put too much load on the API. The API is not supported in any way and will change frequently without warning. If you would like to work with JustWatch and use the data/API please get in contact with them via <a href="mailto:info@justwatch.com">info@justwatch.com</a>. Currently, JustWatch can only work with bigger partners and clients.</p> </blockquote> <p>Thanks to the great folks over at JustWatch, we can query their API for which streaming services have what content. I’ve made use of the filtering that they provide (I’m not made of money unfortunately and can’t subscribe to all streaming services). You can also filter content based on the cost of it, they separate out shows that are included as part of the package and those that are paid (For example, Amazon Prime Video have content that is included in the subscription, and content you need to pay separately for). For my personal use case I’m only interested in content that is either free or included in the subscriptions that I have.</p> <p>There are a few quirks to be aware of when using the JustWatch API (most likely given it’s not really been designed for people like me to use). You’ll notice that we have to filter out some data that is returned but we didn’t search for, as well as making additional requests to get the data that we need to show the content. And given we’re not making JustWatch any money it’s worth pointing out they may turn the service off, require paid API keys or similar. And that’s fine, they’ve invested the money in accumulating the data and are being fairly generous in not stopping the unofficial access.</p> <p>Using the <a href="https://github.com/lufinkey/node-justwatch-api">JustWatch-API</a> package, it’s quite simple for us to query the API.</p> <p>You’ll need to instantiate it with the following, making sure to provide the relevant locale.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">justwatch</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">justwatch-api</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">JustWatch</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">justwatch</span><span class="p">({</span> <span class="na">locale</span><span class="p">:</span> <span class="dl">'</span><span class="s1">en_GB</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div> <p>The search is also relatively straightforward. You’ll need to work out the provider IDs for the services that you’re interested in (just use their website and look at the network requests made in the background).</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">providers</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">al4</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">amp</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">bbc</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">dnp</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">nfx</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">itv</span><span class="dl">'</span><span class="p">];</span> <span class="c1">//All4, Prime Video, BBC iPlayer, Disney+, Netflix, ITVX</span>
<span class="kd">const</span> <span class="nx">monetization_types</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">flatrate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">free</span><span class="dl">'</span><span class="p">];</span> <span class="c1">// Flatrate = included in subscription</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">query</span><span class="p">:</span> <span class="nx">data</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
  <span class="na">providers</span><span class="p">:</span> <span class="nx">providers</span><span class="p">,</span>
  <span class="na">monetization_types</span><span class="p">:</span> <span class="nx">monetization_types</span><span class="p">,</span>
  <span class="na">content_types</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">show</span><span class="dl">'</span><span class="p">],</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">shows</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JustWatch</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
</code></pre></div></div> <p>At this point we’ll get all the results relating to our search term, their search API seems to be optimistic, for example searching for <code class="language-plaintext highlighter-rouge">Buffy the Vampire Slayer</code> will also return <code class="language-plaintext highlighter-rouge">The Vampire Diaries</code>.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">shows</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JustWatch</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">shows</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nf">find</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">item</span><span class="p">.</span><span class="nx">object_type</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">show</span><span class="dl">'</span> <span class="o">&amp;&amp;</span>
    <span class="nx">item</span><span class="p">.</span><span class="nx">original_release_year</span> <span class="o">==</span> <span class="nx">data</span><span class="p">.</span><span class="nx">year</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">episodes</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JustWatch</span><span class="p">.</span><span class="nf">getEpisodes</span><span class="p">(</span><span class="nx">show</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">offer</span> <span class="o">=</span> <span class="nx">episodes</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">offers</span><span class="p">.</span><span class="nf">find</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">x</span> <span class="o">=&gt;</span> <span class="nx">providers</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">x</span><span class="p">.</span><span class="nx">package_short_name</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">monetization_types</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">x</span><span class="p">.</span><span class="nx">monetization_type</span><span class="p">)</span>
<span class="p">})</span>
<span class="k">return</span> <span class="nx">offer</span><span class="p">;</span>
</code></pre></div></div> <p>In that code we’ve cleaned up the returned data, for the purposes of this post I’ve limited it to TV shows, and we’ll filter all the shows returned to any that are in the right year. The rest of it is just filtering to get the correct episode and then filtering out the providers and price points we’re not interested in. Hey Presto, we’ve got our deeplinks!</p> <p>Given how infrequently this data changes, and because we’re all considerate people (we are, right?). It’s worth caching the data that is returned. It’ll prevent their services from being unnecessarily hit with requests for the same data.</p> <h3 id="controlling-the-tv">Controlling the TV</h3> <p>For controlling the TV, we’re going to use a protocol called ADB (Android Debug Bridge), and then we’re going to use a <a href="https://hub.docker.com/r/gounthar/docker-adb">docker container</a> that connects to all of our TVs via ADB, and give’s us a single point of access for our application.</p> <p>Depending on the devices that you want to control, depends on how you enable ADB on that device. The FireTV devices are the easiest!</p> <p>For a Google TV, head to <code class="language-plaintext highlighter-rouge">Settings &gt; Device Preferences &gt; About</code> and click the <code class="language-plaintext highlighter-rouge">Build</code> number several times until a prompt appears confirming developer options have been enabled. Once that’s been done, head back to the menu and you should now have a <code class="language-plaintext highlighter-rouge">Developer Options</code> menu in the <code class="language-plaintext highlighter-rouge">Device Preferences</code> menu.</p> <p>On a FireTV device, head to <code class="language-plaintext highlighter-rouge">Settings &gt; Device &gt; Developer Options</code> and turn on <code class="language-plaintext highlighter-rouge">ADB Debugging</code>. And…. that is all it takes.</p> <p>Given this post isn’t about connecting your android devices to ADB, I’m not going into detail on how it’s done.</p> <p>With all of our TV’s connected, it leaves one more bit of code remaining. Actually sending the deeplink to the device.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">deeplinkCommand</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">deviceClient</span><span class="p">.</span><span class="nf">shell</span><span class="p">(</span>
  <span class="s2">`input keyevent 3 &amp;&amp; am start -W </span><span class="p">${</span><span class="nx">deeplink</span><span class="p">}</span><span class="s2">`</span>
<span class="p">);</span>
</code></pre></div></div> <p>The code above isn’t particuarly complicated, I’m sending the home button to the device (the input keyevent part) to ensure that I’m not in any active application (if you’re in Netflix and send a deeplink for Netflix content then the command is ignored) and then I’m sending the deeplink that has been returned from JustWatch. Your TV should react fairly instantaneously and the content will open.</p> <h2 id="linking-your-echos-to-your-tvs">Linking your Echo’s to your TV’s</h2> <p>This is it, we’re onto the home straight 🥳!</p> <p>There is one final part of the configuration that we need to set up. This will tell the skill which device you want to play the content on when interacting with the echo device. For example, when saying ‘Alexa, stream Buffy the Vampire Slayer from TV Tuner’ to my Alexa in the living room, Alexa will know to instruct our service to start playing content on the TV in the living room.</p> <p><code class="language-plaintext highlighter-rouge">Alexa App -&gt; More -&gt; Settings &gt; TV &amp; Video -&gt; {Skill Name} -&gt; Link or Manage Devices</code></p> <p><img src="/assets/2023-01-31/alexa_screenshot.webp" alt="Configuration in Alexa App" class="align-center"/></p> <p>Now that you’ve got the devices linked, you can speak to each individual echo and just ask it play the content. Try out <code class="language-plaintext highlighter-rouge">Alexa, stream Buffy the Vampire Slayer from {SkillName}</code>. Sit back, get the popcorn and you’re sorted!</p> <h2 id="validating-the-request">Validating the request</h2> <p>With Alexa skills they provide a way of validating that the request is genuinely from Amazon and not spoofed in anyway. <a href="https://developer.amazon.com/en-US/docs/alexa/alexa-skills-kit-sdk-for-nodejs/host-web-service.html#usage-1">More Details</a>. As this just a small project, I just went ahead and implemented a pre-shared key between Haaska and this code. Checking that it exists within the header of the request. If you’re creating a project that will last longer, you should absolutely be validating the timestamp and signature of the incoming request.</p> <h2 id="future-steps">Future Steps</h2> <p>You may have noticed that I’ve cut a certain amount of corners, fine, a lot of corners. I could have spent a long time perfecting this, but in all honesty it has worked 99% of the time that me and my partner have used it. I could have gone with proactive responses, instead of just immediately returning a success to Alexa whilst I try (and potentially fail) to find the right content on a streaming service I have. I could have implemented extra security features <a href="#validating-the-request">as above</a>.</p> <p>If you have any questions about this, reach out and I’ll try to answer them. If you take this project further, I’d love to know how you’ve used it and how it’s worked for you!</p>]]></content><author><name>Mike Grant</name></author><category term="Home Automation"/><category term="Voice Assistant"/><summary type="html"><![CDATA[Building an Alexa Video Skill that can control streaming services on ANY Android based TV.]]></summary></entry><entry><title type="html">Limiting paths that trigger Github Actions</title><link href="https://mikegrant.org.uk/2023/01/26/til-limiting-github-actions-paths.html" rel="alternate" type="text/html" title="Limiting paths that trigger Github Actions"/><published>2023-01-26T00:00:00+00:00</published><updated>2023-01-26T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2023/01/26/til-limiting-github-actions-paths</id><content type="html" xml:base="https://mikegrant.org.uk/2023/01/26/til-limiting-github-actions-paths.html"><![CDATA[<p>Been spending the day experimenting with Github Actions today on a repository that contains multiple backend projects, as well as a front end project. Unfortunately (for investigation another time) the build time is quite slow for the UI. Thankfully we tend to make more backend changes than we do frontend changes and don’t always wait for that pipeline to finish before moving on to working with the deployed code.</p> <p>Thankfully GitHub provide a perfect solution to this problem, not triggering that action if certain files haven’t changed.</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">**.js'</span>
</code></pre></div></div> <p>Bingo, no more pointless UI (or any GitHub action really) builds. More information about the syntax, including including/excluding paths is <a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-including-paths">here</a></p>]]></content><author><name>Mike Grant</name></author><category term="TIL"/><category term="GitHub"/><summary type="html"><![CDATA[Been spending the day experimenting with Github Actions today on a repository that contains multiple backend projects, as well as a front end project. Unfortunately (for investigation another time) the build time is quite slow for the UI. Thankfully we tend to make more backend changes than we do frontend changes and don’t always wait for that pipeline to finish before moving on to working with the deployed code. Thankfully GitHub provide a perfect solution to this problem, not triggering that action if certain files haven’t changed. on: push: paths: - '**.js' Bingo, no more pointless UI (or any GitHub action really) builds. More information about the syntax, including including/excluding paths is here]]></summary></entry><entry><title type="html">2023 Projects</title><link href="https://mikegrant.org.uk/2023/01/01/2023-projects.html" rel="alternate" type="text/html" title="2023 Projects"/><published>2023-01-01T00:00:00+00:00</published><updated>2023-01-01T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2023/01/01/2023-projects</id><content type="html" xml:base="https://mikegrant.org.uk/2023/01/01/2023-projects.html"><![CDATA[<p>I’m not interested in ‘<a href="https://en.wikipedia.org/wiki/New_Year's_resolution">New Years Resolutions</a>’. I’m not patient enough to wait for the new year to make changes in my life. But one thing that has been bugging me slightly over the holiday’s is that I’ve spent a lot of last year wanting to build more things (mainly to teach myself new things) but largely I want to get back the excitement of working on small projects with imperfect code and bugs that I can just live with.</p> <ol> <li><a href="/2023/01/31/controlling-streaming-services-with-alexa-2.html">Voice Assistant control of streaming services</a> <em>This is a bit of a cheat, as I’ve half built this, but I’ve just never finished it</em></li> <li><a href="/2023/02/26/ai-personal-assistant.html">AI personal assistant</a> <em>Probably starting small, e.g. summarizing the day ahead with relevant information</em></li> <li>SignalR based card game (Think Fluxx, Exploding Kittens etc)</li> <li>A Notes app with a focus on collating my terrible handwritten notes.</li> <li>Workout app, it’s probably time to replace my Google Sheets I’ve used for the last couple of years</li> <li>“<a href="https://en.wikipedia.org/wiki/Gamebook">Choose your own adventure</a>” game <em>I used to love these books as a child</em></li> <li>TV/Film Recommendation Engine</li> <li>Browser automation tool <em>I’ve seen a lot of <a href="https://en.wikipedia.org/wiki/Robotic_process_automation">RPA</a> tools as part of my day job and I’m curious what goes into building one</em></li> <li>Democracy chatbot <em>Help people find more information about the political situation in the UK, policies, statements, who / where to vote etc.</em></li> <li>TV/Movie Poster screensaver on my TV <em>Maybe an extension of the recommendation engine</em></li> <li>A collation of APIs to benefit civic engagement and improvement</li> <li>Deprecation tracker accross repositories <em>deprecated dotnet LTS versions etc</em></li> </ol> <p>I’m not saying this will be the list of things that I build in 2023, but for me it’s a start. Throwing these ideas out there gives me a means to hold myself to account. The only thing that I’ll hold myself to is finishing a project a month for the year of 2023.</p>]]></content><author><name>Mike Grant</name></author><summary type="html"><![CDATA[2023 means it's time for something new, 12 months, 12 side projects.]]></summary></entry><entry><title type="html">Merging Swagger Definitions</title><link href="https://mikegrant.org.uk/2022/12/30/til-merging-swagger-definitions.html" rel="alternate" type="text/html" title="Merging Swagger Definitions"/><published>2022-12-30T00:00:00+00:00</published><updated>2022-12-30T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2022/12/30/til-merging-swagger-definitions</id><content type="html" xml:base="https://mikegrant.org.uk/2022/12/30/til-merging-swagger-definitions.html"><![CDATA[<p>Unfortunately Azure API Management doesn’t support swagger files which use a <code class="language-plaintext highlighter-rouge">$ref</code> pointer to reference external files <a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-api-import-restrictions#-openapiswagger-import-limitations">source</a>. Which means to import them I’ll have to manually go through and copy the relevant parts in. Unfortunately the API that I’m working with uses <code class="language-plaintext highlighter-rouge">$ref</code> <strong>a lot</strong>.</p> <p>Thankfully, I found <a href="https://github.com/WindomZ/swagger-merger">Swagger Merger</a> to save me manually copying and pasting accross files!</p> <p>To install it, use the following.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-g</span> swagger-merger
</code></pre></div></div> <p>To merge your multiple files, specify your input with -i and the merged file with -o. It will grab the external <code class="language-plaintext highlighter-rouge">$ref</code> links, download theme and replace the section as required.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>swagger-merger <span class="nt">-i</span> swagger.json <span class="nt">-o</span> swagger-merged.json
</code></pre></div></div>]]></content><author><name>Mike Grant</name></author><category term="TIL"/><category term="OpenAPI"/><category term="Swagger"/><summary type="html"><![CDATA[Unfortunately Azure API Management doesn’t support swagger files which use a $ref pointer to reference external files source. Which means to import them I’ll have to manually go through and copy the relevant parts in. Unfortunately the API that I’m working with uses $ref a lot. Thankfully, I found Swagger Merger to save me manually copying and pasting accross files! To install it, use the following. npm i -g swagger-merger To merge your multiple files, specify your input with -i and the merged file with -o. It will grab the external $ref links, download theme and replace the section as required. swagger-merger -i swagger.json -o swagger-merged.json]]></summary></entry><entry><title type="html">Controlling Home Assistant with GPT-3</title><link href="https://mikegrant.org.uk/2022/12/22/gpt3-and-homeassistant.html" rel="alternate" type="text/html" title="Controlling Home Assistant with GPT-3"/><published>2022-12-22T00:00:00+00:00</published><updated>2022-12-22T00:00:00+00:00</updated><id>https://mikegrant.org.uk/2022/12/22/gpt3-and-homeassistant</id><content type="html" xml:base="https://mikegrant.org.uk/2022/12/22/gpt3-and-homeassistant.html"><![CDATA[<p>For a while I’ve been seeing a lot of great things about GPT3, and it’s created a small fire of interest in my head. I’ve followed some of the work behind <a href="https://promptbase.com/">PromptBase</a> and most recently Nat Friedman’s <a href="https://twitter.com/natfriedman/status/1575631194032549888">tweet</a> about how he was using GPT3 to control a web browser. At this point, it finally clicked for me, an interesting idea to play and learn more about GPT3 with. Using it to control <a href="https://home-assistant.io">Home Assistant</a>!</p> <p><img src="/assets/2022-12-22/openapi.png" alt="OpenAPI Logo - Creators of GPT3"/></p> <h1 id="signing-up">Signing up</h1> <p>To try GPT-3 for free you’ll need a few things, a valid email and phone number which can receive SMS messages. (There are some geo restrictions to be aware of, you can check if your country is on this <a href="https://beta.openai.com/docs/supported-countries">list</a>)</p> <ol> <li>Create an account at <a href="https://beta.openai.com/signup/">https://beta.openai.com/signup/</a></li> <li>You’ll get a confirmation email to verify your account (click the link in the email)</li> <li>You’ll need to provide a real phone number to get a confirmation text.</li> <li>Enter the code that you were sent</li> <li>You’ll get some credit for a new account ($18 i think). Each query uses ‘tokens’ and equates to a cent or so for each execution. Pretty generous and lots of room to experiment and explore the product.</li> </ol> <h1 id="using-the-playground">Using the playground</h1> <p>The playground is the main area where you experiment and build your prompts for use in your applications, allowing you to rapidly iterate your prompts.</p> <p><img src="/assets/2022-12-22/playground.png" alt="Screenshot of the OpenAI playground"/></p> <h1 id="building-my-first-prompt">Building my first prompt</h1> <p>Prompt engineering from what I’ve found, seems to be a dark and unexplained art, but judging from the hiring section on <a href="https://promptbase.com/hire">PromptBase</a> can be quite well paid. There are some examples that people have shared on twitter, some collections on GitHub but I largely have explored this through just playing with prompts and tweaking to find out what has worked.</p> <p>I started with a simple prompt, with fairly low expectaions if I’m honest. What I want to be able to do is pass in unstructured conversational text and get back the HomeAssistant service and the entity_id (reasonably inferred from the text, my Home Assistant entity names are fairly sane)</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OBJECTIVE: turn mike's office light on
device: light.mikes_office_light
command: light.turn_on
===================
</code></pre></div></div> <p>When I pass in the text <code class="language-plaintext highlighter-rouge">turn the snug light on</code> it responded with the device and command. At this point, I’m pretty blown away and can see what a lot of the excitement is about with GPT3!</p> <p><img src="/assets/2022-12-22/first_prompt.png" alt="Results of my first prompt"/></p> <h1 id="building-up-the-prompt">Building up the prompt</h1> <p>I quickly realised that my first prompt (whilst working) wasn’t going to be enough. I would have to parse the response, build a bunch of code. More effort than I wanted to invest in this. So, can GPT-3 give me back the JSON that I wanted? Absolutely it can!</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"objective"</span><span class="p">:</span><span class="w"> </span><span class="s2">"turn mike's office light on"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"device"</span><span class="p">:</span><span class="w"> </span><span class="s2">"light.mikes_office_light"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"light.turn_on"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="err">===================</span><span class="w">
</span></code></pre></div></div> <p><img src="/assets/2022-12-22/updated_prompt.png" alt="Results of updating the prompt"/></p> <p>I would need to test how far this prompt will get me, and teach it about other device and service types. But for a Sunday afternoon project, I’m happy to leave it at this.</p> <h1 id="building-the-app">Building the app</h1> <p>Given GPT-3 is doing all of the heavy lifting for me, it’s possible to use this in a variety of scenarios. It can be hooked up to a telegram bot, Rhasspy or an Alexa skill for example. For now, whilst I’m just using this as a learning opportunity, I’ve built it to accept a simple command line input.</p> <p>The easiest part of this whole process was actually in the integration with <a href="https://home-assistant.io">Home Assistant</a>, their API (documentation can be found <a href="https://developers.home-assistant.io/docs/api/rest/">here</a>) made it very easy. If you’ve been paying attention you’ll recognise some of what that prompt from earlier was trying to achieve. The commands and entity_id are exactly how HomeAssistant expects the data to send via the API.</p> <p>After some processing of the text, and parsing into JSON. We can just use the data we get back from GPT-3 and send it to Home Assistant.</p> <video controls="" class="full" width="100%"> <source src="/assets/2022-12-22/screencast.webm" type="video/webm"/> Your browser does not support the video tag. </video> <h1 id="future">Future</h1> <p>Obviously this is just a very simple implementation, if I get some more time in the future it would be interesting to see how easy it would be to build up more control of more devices without having to write a huge prompt e.g. plugs, thermostats for heating etc.</p> <h3 id="code">Code</h3> <p>You can grab the simple command line app that I built from my <a href="https://github.com/mike-grant/OpenAI-HomeAssistant">GitHub</a>.</p>]]></content><author><name>Mike Grant</name></author><category term="Home Automation"/><category term="AI"/><summary type="html"><![CDATA[Using GPT3 to with minimal prompt engineering to control Home Assistant]]></summary></entry></feed>