Dave Green
Published © GPL3+

Book Club Service Using Amazon DRS

More than just a book club, this service and device will always keep you stocked up in books while helping you find something new.

IntermediateFull instructions provided8 hours940
Book Club Service Using Amazon DRS

Things used in this project

Story

Read more

Schematics

Raspberry Pi GPIO useage

Basic document showing the pins used for the Raspberry PI

Code

Book Selector

C#
This code uses the GoodReads API to get a list of book ISBN's for uploading into DRS. The numbers in each of the string arrays are the author id's from GoodReads. I obtained this by navigating to the author page for each of the chosen authors. For example the first id for thriller authors is 3892 which corresponds to Tom Clancy via the URL https://www.goodreads.com/author/show/3892.Tom_Clancy
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace BookSelector
{
    public class Actions
    {
        private static Random random;
        private string apiAddress = "https://www.goodreads.com/author/list/";
        private string key = "<YOUR_KEY>";
        private string[] thrillerAuthors = { "3892", "2989", "6866", "5194", "18411" };
        private string[] horrorAuthors = { "88506", "3389", "10366", "9355", "6941" };
        private string[] comedyAuthors = { "1654", "4", "7963", "2929", "1221698" };
        private string[] sciFiAuthors = { "4763", "545", "4192148", "51204", "25375" };

        public Actions()
        {
            random = new Random(DateTime.Now.Millisecond);
            random.Next();
        }

        public void GenerateCSV(string filePath)
        {
            var list = GetBookList();
            StringBuilder sb = new StringBuilder();
            foreach(Book b in list)
            {
                sb.AppendLine($"{b.ISBN},{b.Genre},{b.Title.Replace(',', '.')},{b.Author}");
            }
            if (File.Exists(filePath))
                File.Delete(filePath);
            using (var f = File.CreateText(filePath))
            {
                f.Write(sb.ToString());
            }
        }

        public List<Book> GetBookList()
        {
            List<Book> books = new List<Book>();
            books.Add(GetBook(thrillerAuthors[random.Next(0, thrillerAuthors.Length - 1)], "Thriller"));
            books.Add(GetBook(horrorAuthors[random.Next(0, horrorAuthors.Length - 1)], "Horror"));
            books.Add(GetBook(comedyAuthors[random.Next(0, comedyAuthors.Length - 1)], "Comedy"));
            books.Add(GetBook(sciFiAuthors[random.Next(0, sciFiAuthors.Length - 1)], "SciFi"));

            return books;
        }

        public Book GetBook(string authorId, string genre)
        {
            string serviceCall = $"{apiAddress}{authorId}?format=xml&key={key}";
            WebRequest req = WebRequest.Create(serviceCall);
            var resp = (HttpWebResponse)req.GetResponse();
            Stream s = resp.GetResponseStream();
            using (StreamReader sr = new StreamReader(s))
            {
                var y = sr.ReadToEnd();
                var doc = XElement.Parse(y);
                string author = doc.Elements("author").Elements("name").FirstOrDefault().Value;
                var rtn = doc.Elements("author").Elements("books").Elements("book").ToList();
                int count = rtn.Count();
                Random rand = new Random();
                int toPick = rand.Next(count - 1);
                var isbn = rtn[toPick].Element("isbn").Value;
                var title = rtn[toPick].Element("title").Value;
                Book b = new Book(isbn, title, genre, author);

                return b;
            }
        }
    }
}

Main - ServiceActions

C#
This file contains all of the actions to call the DRS services
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace BookClub
{
    public class ServiceActions
    {
        protected JsonSerializerSettings settings;
        private string drsAddress = "https://dash-replenishment-service-na.amazon.com";
        private State state;
        private SaveState saveState;

        public ServiceActions()
        {
            saveState = new SaveState();
            state = saveState.DeserializeState(@"c:\booktest\settings.xml");
            settings = new JsonSerializerSettings();
            settings.NullValueHandling = NullValueHandling.Ignore;
            settings.TypeNameHandling = TypeNameHandling.None;
        }

        public async void Authorize()
        {
            var result = Task.Run(async () => {
                return await this.GetAccessToken();
            }).Result;
            dynamic d = JsonConvert.DeserializeObject(result, settings);
            state.AuthorizationCode = d.access_token;
            state.RefreshToken = d.refresh_token;
            saveState.SerializeState(state);
        }

        public async void RefreshToken()
        {
            var result = Task.Run(async () =>
            {
                return await this.GetRefreshToken();
            }).Result;
            dynamic d = JsonConvert.DeserializeObject(result, settings);
            state.AuthorizationCode = d.access_token;
            state.RefreshToken = d.refresh_token;
            saveState.SerializeState(state);
        }
        public void Replenish(string slotId)
        {
            RefreshToken();
            SendReplenRequest(drsAddress, slotId);
        }

        public void UpdateStatus()
        {
            string json = "{ \"mostRecentlyActiveDate\" : \"" + DateTime.Now + "\"}";
            RefreshToken();
            SendStatusRequest(drsAddress, json);
        }

        protected async Task<string> GetAccessToken()
        {
            string accessAddress = "https://api.amazon.com/auth/o2/token";
            Auth auth = new Auth("authorization_code", state.AccessToken, state.ClientId, state.ClientSecret, "https%3A%2F%2Flocalhost");
            return await GetServiceResponse(accessAddress, auth.GetBody());
        }

        protected async Task<string> GetRefreshToken()
        {
            string accessAddress = "https://api.amazon.com/auth/o2/token";
            Auth auth = new BookClub.Auth("refresh_token", state.RefreshToken, state.ClientId, state.ClientSecret, "https%3A%2F%2Flocalhost");
            return await GetServiceResponse(accessAddress, auth.GetBody());
        }
        protected async Task<string> GetServiceResponse(string address, string body)
        {
            var request = (HttpWebRequest)WebRequest.Create(address);
            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "POST";
            request.Headers["Cache-Control"] = "no-cache";
            var data = System.Text.Encoding.UTF8.GetBytes(body);
            using (var sw = new StreamWriter(await request.GetRequestStreamAsync()))
            {
                sw.Write(body);
            }
            try
            {
                using (var response = await request.GetResponseAsync())
                {
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        return (sr.ReadToEnd());
                    }
                }
            }
            catch (WebException ex)
            {
                var resp = ex.Response as HttpWebResponse;
                using (var sr = new StreamReader(resp.GetResponseStream()))
                {
                    string s = sr.ReadToEnd();
                }
            }
        }

        protected async void SendReplenRequest(string address, string slotId)
        {
            var request = (HttpWebRequest)WebRequest.Create(address + "/replenish/" + slotId);
            request.ContentType = "application/json";
            request.Headers["x-amzn-accept-type"] = "com.amazon.dash.replenishment.DrsReplenishResult@1.0";
            request.Headers["x-amzn-type-version"] = "com.amazon.dash.replenishment.DrsReplenishInput@1.0";
            request.Headers["Authorization"] = "Bearer " + state.AuthorizationCode;
            request.Method = "POST";
            try
            {
                var response = await request.GetResponseAsync() as HttpWebResponse;
                    using (var sr = new StreamReader(response.GetResponseStream()))
                    {
                        var s =  (sr.ReadToEnd());
                    }
            }
            catch (WebException ex)
            {
                var resp = ex.Response as HttpWebResponse;
                using (var sr = new StreamReader(resp.GetResponseStream()))
                {
                    string s = sr.ReadToEnd();
                }
            }
        }

        protected async void SendStatusRequest(string address, string jsonBody)
        {
            var request = (HttpWebRequest)WebRequest.Create(address);
            request.ContentType = "application/json";
            request.Headers["x-amzn-accept-type"] = "com.amazon.dash.replenishment.DrsDeviceStatusResult@1.0";
            request.Headers["x-amzn-type-version"] = "com.amazon.dash.replenishment.DrsDeviceStatusInput@1.0";
            request.Headers["Authorization"] = "Bearer " + state.AuthorizationCode;
            request.Method = "POST";
            using (var sw = new StreamWriter(await request.GetRequestStreamAsync()))
            {
                sw.Write(jsonBody);
            }
            try
            {
                var response = await request.GetResponseAsync() as HttpWebResponse;
            }
            catch (WebException ex)
            {
                var resp = ex.Response as HttpWebResponse;
                using (var sr = new StreamReader(resp.GetResponseStream()))
                {
                    string s = sr.ReadToEnd();
                }
            }
        }
    }
}

Book Selector - Test

C#
I use a unit test to run this. The intention going forward would be for a management dashboard, but is dependent on seeing what if any changes are made to the DRS API's in the near future. This would then determine if this was to be part of an automated process such as PowerShell or bash, or if it was to continue being a manual process.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using BookSelector;
using System.IO;

namespace BookSelector.Tests
{
    [TestClass]
    public class ActionsTests
    {
        [TestMethod]
        public void TestMethod1()
        {
            Actions a = new Actions();
            a.GenerateCSV(@"c:\booktest\latest.csv");
            Assert.IsTrue(File.Exists(@"c:\booktest\latest.csv"));
        }
    }
}

Main - Auth

C#
Helper classed used in main the get the request bodies for calls to obtain auth and refresh tokens
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BookClub
{
    public class Auth
    {
        public Auth(string grant_type, string code, string client_id, string client_secret, string redirect_url)
        {
            this.grant_type = grant_type;
            this.code = code;
            this.client_id = client_id;
            this.client_secret = client_secret;
            this.redirect_uri = redirect_url;
        }
        public string grant_type { get; set; }
        public string code { get; set; }
        public string client_id { get; set; }
        public string client_secret { get; set; }
        public string redirect_uri { get; set; }

        public string GetBody()
        {
            if (grant_type != "refresh_token")
                return $"grant_type={grant_type}&code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}";
            else
                return $"grant_type={grant_type}&refresh_token={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}";
        }
    }
}

Main - Slot

C#
Basic class to hold slot values
    public class Slot
    {
        public Slot(int slotNumber, string name, string id)
        {
            SlotNumber = slotNumber;
            Name = name;
            SlotID = id;
        }
        public int SlotNumber { get; set; }
        public string Name { get; set; }
        public string SlotID { get; set; }
    }

Main - MainPage.xaml

XML
XAML main page for display on Raspberry Pi
<Page
    x:Class="BookClub.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BookClub"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="FontSize" Value="35"/>
        </Style>
    </Page.Resources>
    <Grid Background="Black">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock Foreground="Yellow">Smart Book Club</TextBlock>
        <TextBlock Grid.Row="1" Name="Order"/>
        <TextBlock Grid.Row="2" Name="ChosenSlot"/>
    </Grid>
</Page>

Main - MainPage.xaml.cs

C#
The code behind file for MainPage.xaml. This contains the code for accessing the GPIO pins on the device and making the calls to ServiceActions to order via DRS
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Devices.Gpio;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace BookClub
{
    public sealed partial class MainPage : Page
    {
        private const int BUTTON_PIN = 5;
        private const int SELECTOR_PIN = 4;
        private GpioPin buttonPin;
        private GpioPin selectorPin;
        private static List<Slot> slots;
        private static int SelectedSlot = 1;
        private static DispatcherTimer orderTimer;

        public MainPage()
        {
            this.InitializeComponent();
            try
            {
                this.Initialize();
                slots = new List<Slot>();
                slots.Add(new Slot(1, "Thriller", "05709bc7-83d3-47b6-be79-619555e15698"));
                slots.Add(new Slot(2, "Horror", "6eea7f46-0691-48b3-8edc-fbb9b8a44c5a"));
                slots.Add(new Slot(3, "Comedy", "35cd98c5-050d-4934-a20b-479213ce5768"));
                slots.Add(new Slot(4, "SciFi", "1494afb3-0e95-4212-8450-f3302bae7c32"));
            }
            catch (Exception ex)
            {
                Order.Text = ex.Message;
            }
        }

        private void Initialize()
        {
            var gpio = GpioController.GetDefault();

            if (gpio == null)
            {
                throw new Exception("There is no GPIO controller on this device.");
            }

            buttonPin = gpio.OpenPin(BUTTON_PIN);
            if (buttonPin.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
                buttonPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
            else
                buttonPin.SetDriveMode(GpioPinDriveMode.Input);
            buttonPin.DebounceTimeout = TimeSpan.FromMilliseconds(50);
            buttonPin.ValueChanged += ButtonPin_ValueChanged;

            selectorPin = gpio.OpenPin(SELECTOR_PIN);
            if (selectorPin.IsDriveModeSupported(GpioPinDriveMode.InputPullUp))
                selectorPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
            else
                selectorPin.SetDriveMode(GpioPinDriveMode.Input);
            selectorPin.DebounceTimeout = TimeSpan.FromMilliseconds(50);
            selectorPin.ValueChanged += SelectorPin_ValueChanged;

            InitializeStatusTimer();
            InitializeOrderTimer();
        }

        private void InitializeStatusTimer()
        {
            DispatcherTimer dt = new DispatcherTimer();
            dt.Tick += StatusTimerTick;
            dt.Interval = new TimeSpan(12, 0, 0);
            dt.Start();
        }

        private void StatusTimerTick(object sender, object e)
        {
            SendStatus();
        }

        private void InitializeOrderTimer()
        {
            orderTimer = new DispatcherTimer();
            orderTimer.Tick += OrderTimerTick;
            orderTimer.Interval = new TimeSpan(0, 0, 30);
        }

        private void OrderTimerTick(object sender, object e)
        {
            var slotId = slots.Where(s => s.SlotNumber == SelectedSlot).FirstOrDefault().SlotID;
            MakeOrder(slotId);
            orderTimer.Stop();
            var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                Order.Text = "An Order has been placed";
            });
        }

        private void SelectorPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
        {
            var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                if (args.Edge == GpioPinEdge.FallingEdge)
                {
                    if (SelectedSlot == slots.Count)
                    {
                        SelectedSlot = 1;
                    }
                    else
                    {
                        SelectedSlot++;
                    }
                    var slotName = slots.Where(s => s.SlotNumber == SelectedSlot).FirstOrDefault().Name;
                    ChosenSlot.Text = "Selected slot is:  " + slotName;
                }
            });
        }

        private void ButtonPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
        {
            var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                if (args.Edge == GpioPinEdge.FallingEdge)
                {
                    Order.Text = "A book is in the holder";
                }
                else
                {
                    Order.Text = "An order is about to be made";
                    orderTimer.Start();
                }
            });
        }

        public void MakeOrder(string slotId)
        {
            ServiceActions actions = new ServiceActions();
            actions.Replenish(slotId);
        }

        public void SendStatus()
        {
            ServiceActions actions = new ServiceActions();
            actions.UpdateStatus();
        }
    }
}

UserLogin

HTML
This is a basic html file with javascript to allow login with amazon to configure your DRS device
<!DOCTYPE html>
<html>
<head>
    <title></title>
	<meta charset="utf-8" />
</head>
<body>
    <div id="amazon-root"></div>
    <script type="text/javascript">

      window.onAmazonLoginReady = function() {
          amazon.Login.setClientId('<yourclientid>');
    };
    (function(d) {
        var a = d.createElement('script'); a.type = 'text/javascript';
        a.async = true; a.id = 'amazon-login-sdk';
        a.src = 'https://api-cdn.amazon.com/sdk/login1.js';
        d.getElementById('amazon-root').appendChild(a);
    })(document);

    </script>
    <a href="#" id="LoginWithAmazon">
        <img border="0" alt="Login with Amazon"
             src="https://images-na.ssl-images-amazon.com/images/G/01/lwa/btnLWA_gold_156x32.png"
             width="156" height="32" />
    </a>
    <script type="text/javascript">

        document.getElementById('LoginWithAmazon').onclick = function () {
            var options = new Object();
            var scope = ('dash:replenish');
            var scope_data = new Object();
            scope_data['dash:replenish'] = { "device_model": "<yourdevicemodel>", "serial": "<yourserial>" };
            options['scope_data'] = scope_data;
            options['scope'] = scope;
            options['response_type'] = 'code';
            amazon.Login.authorize(options, "<yourredirecturi>");
        return false;
        };

    </script>
</body>
</html>

Credits

Dave Green

Dave Green

2 projects • 4 followers

Comments