Gary Ritter Jr
Published

Halcyon

Smart thermostat with Microsoft calendar integration

BeginnerWork in progress2,681
Halcyon

Things used in this project

Hardware components

Raspberry Pi 2 Model B
Raspberry Pi 2 Model B
×1
Microchip MCP3008
10 bit, 8 channel, analog to digital converter
×1
TMP36G
Analog temperature sensor that outputs in Celsius from -50C to 150C
×1
Relay (generic)
I'm using a 2 channel relay board from http://www.ebay.com/itm/231576878934?_trksid=p2057872.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT
×1
LED (generic)
LED (generic)
I'm using one red and one blue
×2
Jumper wires (generic)
Jumper wires (generic)
×25
Adafruit T-Cobbler Plus
×1

Software apps and online services

Windows 10 IoT Core
Microsoft Windows 10 IoT Core

Story

Read more

Schematics

Prototype Hardware Configuration

This is the hardware connections for my Halcyon prototype. With this, I am lighting blue and red LEDs instead of actual AC and heater during testing.

Early hardware test picture

Early hardware test all wired up and working.

Relay Module

close up of the relay module I'm using. It's possible to breadboard your own version of this but it's also very inexpensive to buy a prebuilt module like this one.

Prototype Hardware Configuration 2

View of the first prototype at an angle. Note that my first prototype only was controlling a single LED and I wired it directly to the Raspberry Pi instead of using the Adafruit Cobbler. The cobbler is AMAZING.

Blinky

For my first ever test doing anything at all on a Raspberry Pi, I made a version of the Microsoft Blinky sample to flash a blue LED. Worked like a charm. I was so excited!

Code

HalcyonHardware.vb

VBScript
This module contains code that interacts with the hardware elements. Language is VB.NET
Imports Windows.Devices.Gpio
Imports Windows.Devices.Spi
Imports Windows.Devices.Enumeration

'This module is for any code that directly interacts with hardware

Module HalcyonHardware
    Private Const RELAY_PIN_AC_NUM As Integer = 26 'Use GPIO number, not literal RPi pin number
    Private Const RELAY_PIN_HEAT_NUM As Integer = 13 'Use GPIO number, not literal RPi pin number
    Private RELAY_PIN_AC 'The pin object that we will use to send commands
    Private RELAY_PIN_HEAT 'The pin object that we will use to send commands
    Private SpiDisplay As SpiDevice
    Private readBuffer As Byte() = New Byte(2) {} 'this Is defined to hold the output data from MCP3008
    Private writeBuffer As Byte() = New Byte(2) {&H1, &H80, &H0} 'It Is SPI port serial input pin, And Is used To load channel configuration data into the device


    Public Sub InitializeHardware()
        InitializeRelayPins() 'This gets our GPIO pins ready to trigger the relays
        InitializeSpiDisplay() 'This gets our SPI ready to read data from the MCP3008
    End Sub

    Public Sub ToggleRelays()
        'This will turn devices plugged into the relays on or off
        'low = off; high = on
        If ac_ON Then RELAY_PIN_AC.write(GpioPinValue.High) Else RELAY_PIN_AC.write(GpioPinValue.Low)
        If heat_ON Then RELAY_PIN_HEAT.write(GpioPinValue.High) Else RELAY_PIN_HEAT.write(GpioPinValue.Low)
    End Sub

    Private Function ByteToInt(data As Byte()) As Integer
        'Comments assume readBuffer values of {00000000 , 00000010, 00110001} (0,2,49)

        'imagine all three separate bytes of readbuffer just need to be combined into one long "string" to equal the REAL binary value. Since we know the MCP3008 outputs 10 bits,
        '   we can safely ignore the first byte entirely as it should always be all zeros
        'So 10 bits = the entire 3rd byte (8 bits) and the least two positions of the second byte. First byte is unused.
        Dim result As Integer = data(1) And &H3 'this combines the second byte with hexadecimal 3 (3 in decimal, 0011 in binary) which will result in major six positions returning zero and minor two positions keeping value
        result <<= 8 'This shifts those bits to the left by 8 places leaving an empty byte of zeros (00000010 -> 00000010 00000000)
        result += data(2) 'This fills that empty byte of zeros with the values from the third byte in the readBuffer, generating the stitched back together binary number that has 10 bits (plus an additional 4 major zero bits).
        '(00000010 00000000 -> 00000010 00110001 = 561 in decimal)
        Return result
    End Function

    Public Sub GetTemp()
        Dim rawInt As Integer = 0
        SpiDisplay.TransferFullDuplex(writeBuffer, readBuffer) 'writeBuffer programs the MCP3008 to return channel 0. That 10 bit data gets encoded into the readBuffer in the second two bytes

        rawInt = ByteToInt(readBuffer) '10 bits from MCP3008 range 0 - 1023
        TempAmbient_Celsius = Math.Round((((rawInt / 1024) * power_mVolts) - 500) / 10, precision)
    End Sub

    Private Sub InitializeRelayPins()
        'This initializes the GPIO pins which will be used to activate the relays

        Dim gpio = GpioController.GetDefault()
        If gpio Is Nothing Then
            Throw New Exception("GPIO not found")
        End If

        RELAY_PIN_AC = gpio.OpenPin(RELAY_PIN_AC_NUM)
        If RELAY_PIN_AC Is Nothing Then
            Throw New Exception("AC Relay Pin Error. GPIO=" & RELAY_PIN_AC_NUM)
        End If
        RELAY_PIN_AC.Write(GpioPinValue.Low) 'default to low
        RELAY_PIN_AC.SetDriveMode(GpioPinDriveMode.Output)

        RELAY_PIN_HEAT = gpio.OpenPin(RELAY_PIN_HEAT_NUM)
        If RELAY_PIN_HEAT Is Nothing Then
            Throw New Exception("Heat Relay Pin Error. GPIO=" & RELAY_PIN_HEAT_NUM)
        End If
        RELAY_PIN_HEAT.Write(GpioPinValue.Low) 'default to low = off
        RELAY_PIN_HEAT.SetDriveMode(GpioPinDriveMode.Output)
    End Sub

    Private Async Sub InitializeSpiDisplay()
        'This initializes the SPI interface used to communicate with MCP3008

        Const SPI_CONTROLLER_NAME As String = "SPI0" 'For Raspberry Pi 2, use "SPI0"
        Const SPI_CHIP_SELECT_LINE As Int16 = 0 'Line 0 maps To physical pin number 24 On the Rpi2

        Try
            Dim settings = New SpiConnectionSettings(SPI_CHIP_SELECT_LINE)
            settings.ClockFrequency = 500000 'this should match your SPI Bus speed
            settings.Mode = SpiMode.Mode0
            Dim spiAqs As String = SpiDevice.GetDeviceSelector(SPI_CONTROLLER_NAME)
            Dim deviceInfo = Await DeviceInformation.FindAllAsync(spiAqs)
            SpiDisplay = Await SpiDevice.FromIdAsync(deviceInfo(0).Id, settings)

        Catch ex As Exception
            Throw New Exception("SPIO Error")
        End Try



    End Sub
End Module

HalcyonSettings.vb

VBScript
This module contains all the shared settings for Halcyon. Language is VB.NET
'This module is for storing all the shared variables used in the program


'All settings are hardcoded and volatile for now. Later they will be configurable from your phone or web app

Module HalcyonSettings
    Public UpdateTemp_Seconds As Integer = 1 'frequency of ambient temp updates
    Public UpdateCal_Minutes As Integer = 60 'frequency of calendar sync updates

    Public HomeIndicator As String() = {"26H", "Piscassic", "Gary"} 'if event location CONTAINS any of these strings, it's home

    Public Const power_mVolts As Double = 5000 '3300 or 5000 depending on power supply to TMP36 and MCP3008

    Public ac_ON As Boolean = False
    Public heat_ON As Boolean = False

    Public AllCalendars As List(Of HalcyonCalendar) 'This is a list of all calendars that should be considered, for the prototype it is just my own
    Public CurrentEvent As HalcyonCalendarEvent 'This is the current HalcyonCalendarEvent that the temp setting is based on
    Public Const precision As Integer = 2 'number of decimal places to always round the temp to

    Private ActualTolerance_Celsius As Double = 1 'degrees Celsius of tolerance
    Public Property TempTolerance_Celsius As Double 'interfaces with ActualTolerance_Celsius to ensure nothing below 1 or above 10
        Get
            Return ActualTolerance_Celsius
        End Get
        Set(value As Double)
            If value < 1 Then value = 1
            If value > 10 Then value = 10
            ActualTolerance_Celsius = value
        End Set
    End Property

    Public TempAmbient_Celsius As Double = 20 'this variable will hold the latest ambient temperature reading

    Private ActualSet_Celsius As Double = 20 'degrees celsius of currently set temperature
    Public Property TempSet_Celsius As Double 'interfaces with ActualSet_Celsius to enforce safety min/max
        Get
            Return ActualSet_Celsius
        End Get
        Set(value As Double)
            If value < 1 Then value = 1 '1=33F
            If value > 37 Then value = 37 '37=100F
            ActualSet_Celsius = value
        End Set
    End Property

    Public CalendarLinks As String() = {
       "https://sharing.calendar.live.com/calendar/private/82856ffe-1447-4134-9164-2690d5cee38e/7661b193-8977-40ce-980f-1e20ff46d8ae/cid-60e973077a985b49/calendar.xml",
       "https://sharing.calendar.live.com/calendar/private/eca6cd71-ae7c-4146-b7c1-a8f9d03de0d2/0ff376e0-b10d-4067-acde-71cd450283db/cid-60e973077a985b49/calendar.xml"}

    Public TempEmpty_Min_Celsius As Double = 12 'min temp when house is empty in MinMax mode
    Public TempEmpty_Max_Celsius As Double = 10 'max temp when house is empty in MinMax mode
    Public TempOccupied_Day_Celsius As Double = 21 'Desired daytime temp
    Public TempOccupied_Night_Celsius As Double = 16 'Desired nighttime temp
    Public TempUnoccupied_Celsius As Double = 15 'Desired temp when house is empty in KeepTemp mode

    Public UnoccupiedMode As DecisionMode = DecisionMode.MinMax 'How to behave when house is empty
    Public PartyMode As PartyShift_Celsius = PartyShift_Celsius.Slight
    Public PartyShift_Custom_Celsius As Double = 200

    Public StartTime_Day As DateTime 'if current time is > StartTime_Day AND < StartTime_Night. It's Day
    Public StartTime_Night As DateTime 'if current time is > StartTime_Night OR < StartTime_Day, It's Night

    Public Enum DecisionMode
        KeepTemp 'This will keep temp within tolerance to current setting
        MinMax 'This will stop temp from falling outside of range, saves energy while unoccupied
        DoNothing 'Will never activate heating or cooling, best energy savings while unoccupied
    End Enum

    Public Enum PartyShift_Celsius
        'extra attendees at a home event will be cause the temp to be DECREASED by this/100 degrees
        None = 000 'no change
        Slight = 025 'about 0.5 F
        Moderate = 050 'about 1.0 F
        Aggressive = 075 'about 1.5 F
        Custom = 999 'user defined
    End Enum

    Public ReadOnly Property TempAmbient_Farenheit As Double
        'Returns the most recent ambient temperature reading converted to Farenheit
        'If you're using a temperature sensor that reads in farenheit instead of celsius,
        '   make this Property Not ReadOnly And uncomment the Set method so you can write to it
        Get
            Return TempConverter(TempAmbient_Celsius, True)
        End Get
        'Set(value As Double)
        '    TempAmbient_Celsius = TempConverter(value, False)
        'End Set
    End Property

    Public Property TempTolerance_Farenheit As Double
        'Allows use of TempTolerance_Celsius using Farenheit
        Get
            Return TempConverter(TempTolerance_Celsius, True)
        End Get
        Set(value As Double)
            TempTolerance_Celsius = TempConverter(value, False)
        End Set
    End Property

    Public Property TempSet_Farenheit As Double
        'Allows use of TempSet_Celsius using Farenheit
        Get
            Return TempConverter(TempSet_Celsius, True)
        End Get
        Set(value As Double)
            TempSet_Celsius = TempConverter(value, False)
        End Set
    End Property
End Module

HalcyonSoftware.vb

VBScript
This module contains all the code that is not hardware related. Language is VB.NET
'This module is for any code that does NOT interact with hardware

Module HalcyonSoftware
    Private TimerTemp As DispatcherTimer 'Timer to update ambient temperature
    Private TimerCal As DispatcherTimer 'Timer to sync calendar data

    Public Sub InitializeSoftware()
        InitializeCalendars()
        UpdateCurrentEvent()
        InitializeTimers()
    End Sub

    Public Sub stopTimers()
        On Error Resume Next
        TimerTemp.Stop()
        TimerCal.Stop()
        On Error GoTo 0
    End Sub


    Private Sub InitializeCalendars()
        AllCalendars = New List(Of HalcyonCalendar)

        For Each CalLink As String In CalendarLinks
            Dim newCal As HalcyonCalendar = New HalcyonCalendar

            newCal.str_Link = CalLink.Trim
            newCal.Resync()
            AllCalendars.Add(newCal)
        Next

        UpdateCurrentEvent()
    End Sub

    Public Function TempConverter(degrees As Double, CtoF As Boolean) As Double
        If CtoF Then 'converting Celsius to Farenheit
            Return Math.Round((degrees * 9 / 5) + 32, precision)
        Else 'converting Farenheit to Celsius
            Return Math.Round((degrees - 32) * 5 / 9, precision)
        End If
    End Function

    Private Sub InitializeTimers()
        'This initiates the timers used to get temp updates and calendar syncs
        TimerTemp = New DispatcherTimer
        TimerTemp.Interval = New TimeSpan(0, 0, 0, UpdateTemp_Seconds)
        AddHandler TimerTemp.Tick, AddressOf TimerTemp_Tick
        TimerTemp.Start()

        TimerCal = New DispatcherTimer
        TimerCal.Interval = New TimeSpan(0, 0, UpdateCal_Minutes, 0)
        AddHandler TimerCal.Tick, AddressOf TimerCal_Tick
        TimerCal.Start()
    End Sub

    Private Sub TimerTemp_Tick()
        'If the current event has ended, then we need to update that. 
        If CurrentEvent IsNot Nothing AndAlso CurrentEvent.dt_EndTime < DateTime.Now Then
            TimerTemp.Stop() 'Calendar update could take a moment, don't want the timer to keep firing while updating
            UpdateCurrentEvent()
            TimerTemp.Start()
        End If
        GetTemp()
        MakeDecision()
    End Sub

    Private Sub MakeDecision()
        'Should be called after getting an updated temp
        'Will toggle heat and AC on or off based on current temp

        Try

            If CurrentEvent.AtHome Then
                If DateTime.Now.TimeOfDay > StartTime_Day.TimeOfDay And DateTime.Now.TimeOfDay < StartTime_Night.TimeOfDay Then
                    TempSet_Celsius = TempOccupied_Day_Celsius
                Else
                    TempSet_Celsius = TempOccupied_Night_Celsius
                End If

                'for party mode
                'For example, slight = 025
                '025 / 100 = 0.25
                'if 3 people are attending, subtract one for the owner
                '3 - 1 = 2 people extra, * .25 = 0.50 temp adjustment
                TempSet_Celsius -= (PartyMode / 100) * Math.Max((CurrentEvent.int_Attending - 1), 0)
            Else
                Select Case UnoccupiedMode
                    Case DecisionMode.DoNothing
                        TempSet_Celsius = TempAmbient_Celsius 'if setting == current temp then it will do nothing
                    Case DecisionMode.KeepTemp
                        TempSet_Celsius = TempUnoccupied_Celsius 'keep the unoccupied setting
                    Case DecisionMode.MinMax
                        If TempAmbient_Celsius < TempEmpty_Min_Celsius Then
                            TempSet_Celsius = TempEmpty_Min_Celsius 'we are below min so set to min
                        ElseIf TempAmbient_Celsius > TempEmpty_Max_Celsius Then
                            TempSet_Celsius = TempEmpty_Max_Celsius 'we are above max so set to max
                        Else
                            TempSet_Celsius = TempAmbient_Celsius 'do nothing
                        End If
                End Select
            End If
        Catch ex As Exception
            'in case of error, just call me occupied
            TempSet_Celsius = TempOccupied_Day_Celsius
        Finally
            'Simple if logic checks if we're more than tolerance away from setting
            If TempAmbient_Celsius < (TempSet_Celsius - TempTolerance_Celsius) Then heat_ON = True Else heat_ON = False
            If TempAmbient_Celsius > (TempSet_Celsius + TempTolerance_Celsius) Then ac_ON = True Else ac_ON = False
            ToggleRelays() 'Turn on or off as needed
        End Try
    End Sub

    Private Sub TimerCal_Tick()
        'resync all the calendars
        For Each oneCal As HalcyonCalendar In AllCalendars
            oneCal.Resync()
        Next
        'update the current event based on new calendar data
        UpdateCurrentEvent()
        'make a decision in case current event changed
        MakeDecision()
    End Sub

    Private Sub UpdateCurrentEvent()

        Dim mainEvent As HalcyonCalendarEvent = New HalcyonCalendarEvent
        mainEvent.dt_StartTime = DateTime.Now
        mainEvent.dt_EndTime = DateTime.Now.AddYears(1).AddHours(1) 'puts the date 1 year and 1 day into the future. to be replaced by found events
        mainEvent.AtHome = False

        For Each oneCal As HalcyonCalendar In AllCalendars
            For Each oneEvent As HalcyonCalendarEvent In oneCal.obj_Events
                If oneEvent.dt_EndTime < DateTime.Now Or oneEvent.dt_StartTime < DateTime.Now Then
                    'Event is either in the past or the future, ignore it
                    Continue For
                End If

                'This event is currently happening! YAY!

                If oneEvent.AtHome Then
                    If mainEvent.AtHome Then
                        'we are overwriting data because this is the first home event found
                        mainEvent.AtHome = True
                        mainEvent.int_Attending += oneEvent.int_Attending
                        'we keep the lowest end time so we can update the party temp when one overlapping event ends
                        If oneEvent.dt_EndTime < mainEvent.dt_EndTime Then mainEvent.dt_EndTime = oneEvent.dt_EndTime
                    End If
                Else
                    'if multiple events overlap (for example, from different calendars), then the HOME event has priority
                    If mainEvent.AtHome = True Then
                        'a home event already has priority so skip this away event
                        Continue For
                    Else
                        mainEvent.str_Location = "Away"
                        mainEvent.AtHome = False
                        If oneEvent.dt_EndTime < mainEvent.dt_EndTime Then mainEvent.dt_EndTime = oneEvent.dt_EndTime
                    End If
                End If
            Next
        Next
    End Sub

    Public Function IsAtHome(location_str As String) As Boolean
        For Each s As String In HomeIndicator
            If location_str.ToLower.Contains(s.ToLower) Then Return True
        Next
        Return False
    End Function
End Module

HalcyonCalendar.vb

VBScript
This class defines the HalcyonCalendar object and HalcyonCalendarEvent objects. Because I don't know how to actually link up to a live calendar feed and iterate, I'm attempting to simulate the functionality by parsing the shared XML data from a calendar. Language is VB.NET
Imports System.Net.Http

Public Class HalcyonCalendar

    Private xDoc As XDocument = Nothing

    Public Property obj_Events As List(Of HalcyonCalendarEvent)
    Public Property str_TimeZoneOffset As Integer
    Public Property str_Link As String
    Public Property dt_Updated As DateTime
    Public Property str_Title As String
    Public Property str_RawXML As String
    'timezone?

    Public Sub Resync()
        Dim client As New HttpClient
        Dim resp = client.GetAsync(str_Link)
        str_RawXML = resp.Result.Content.ReadAsStringAsync.Result
        Dim doc As New XDocument
        doc = XDocument.Parse(str_RawXML)
        Dim temporaryCal As HalcyonCalendar = Iterate_live(doc)
        obj_Events = New List(Of HalcyonCalendarEvent)
        obj_Events = temporaryCal.obj_Events
    End Sub

    Public Sub New()
        Me.obj_Events = New List(Of HalcyonCalendarEvent)
        Me.str_Title = "new calendar: title not set"
        Me.str_Link = "new calendar: link not set"
    End Sub

    Public Sub AddEvent(obj As HalcyonCalendarEvent)
        obj_Events.Add(obj)
    End Sub
End Class

Public Class HalcyonCalendarEvent
    Public Property dt_StartTime As DateTime
    Public Property dt_EndTime As DateTime
    Public Property str_Location As String
    Public Property int_Attending As Integer
    Public Property str_Title As String
    Public Property str_RawContent As String
    Public Property dt_Updated As DateTime
    Public Property AtHome As Boolean
    'timezone?

    Public Sub New(StartTime As DateTime, EndTime As DateTime, Location As String, Optional Attending As Integer = 1, Optional AllDay As Boolean = False)
        If AllDay Then
            Me.dt_StartTime = New DateTime(StartTime.Year, StartTime.Month, StartTime.Day, 0, 0, 1)
            Me.dt_EndTime = New DateTime(StartTime.Year, StartTime.Month, StartTime.Day, 23, 59, 59)
        Else
            Me.dt_StartTime = StartTime
            Me.dt_EndTime = EndTime
        End If
        Me.str_Location = Location
        Me.int_Attending = Attending
    End Sub

    Public Sub New()
    End Sub
End Class

IterateXML.vb

VBScript
This module is for iterating calendar XML data from a Microsoft Live calendar share link. I decided to put it in it's own module because 1- it's big and nasty, 2- as soon as i can actually link a calendar it can be deleted. Language is VB.NET
'This module is responsible for iterating the calendar XML. It's complicated enough that I want it isolated for easier reading.
'In a future version, XML parsing will be replaced with actual Microsoft Account linking

Public Module IterateXML
    Public Function Iterate_live(xDoc As XDocument) As HalcyonCalendar
        '<?xml version="1.0" encoding="utf-8"?>
        'feed
        '   title
        '   link
        '   updated
        '   entry
        '       title
        '       updated
        '       author
        '           email
        '       content
        '   timezone

        Dim cal As New HalcyonCalendar
        'pull raw info out of XML
        For Each oneElement As XElement In xDoc.Root.Descendants()
            Select Case oneElement.Name.LocalName
                Case "link"
                    'we already know this
                Case "title"
                    cal.str_Title = oneElement.Value
                Case "updated"
                    'cal.dt_Updated = 
                    DateTime.TryParse(oneElement.Value, cal.dt_Updated)
                Case "entry"
                    Dim newEntry As New HalcyonCalendarEvent
                    newEntry.int_Attending = 0
                    For Each entryElement As XElement In oneElement.Descendants
                        Select Case entryElement.Name.LocalName
                            Case "title"
                                newEntry.str_Title = entryElement.Value
                            Case "updated"
                                newEntry.dt_Updated = DateTime.Parse(entryElement.Value)
                            Case "content"
                                newEntry.str_RawContent = entryElement.Value
                            Case Else
                                'not used
                                'author
                        End Select
                    Next
                    cal.obj_Events.Add(newEntry)
                Case Else
                    'not used
            End Select
        Next

        'parse event content to fill in remaining calendarEvent data

        For Each oneEvent As HalcyonCalendarEvent In cal.obj_Events
            'Original
            '&lt;table style="font-family:tahoma;font-size:11px"&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Start:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Thursday, August 16, 2012&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;End:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Thursday, August 16, 2012&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Location:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;St Louis&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Who:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Aleisha Ritter;undertkr2002@yahoo.com&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td /&gt;&lt;/tr&gt;&lt;/table&gt;

            'HTML replacements IN THIS ORDER!
            '   "find!replace"
            '   "find!"            to leave blank
            '   "find!|"      for separator
            Dim replaceStrings() As String = {"&lt;!<", "&gt;!>", "<tr>!", "</tr>!", "<b>!", "</b>!", "<td style=""width:10px"" align=""right"" />!", "<td />!", "style=""vertical-align:top""!", "align=""right""|", "style=""font-family:tahoma;font-size:11px""!", "td>!", "<td!", "<table >!", "</table>!", ">!", "<!", "/td!|"}

            Dim tempStr As String()
            Dim tempContent As String

            tempContent = oneEvent.str_RawContent
            For Each oneString As String In replaceStrings
                Try
                    tempContent = tempContent.Replace(oneString.Split("|")(0), oneString.Split("|")(1))
                Catch ex As Exception
                End Try
            Next

            'After replacements we can just split on |, trim each string, and iterate through it
            'Start:|Thursday, August 16, 2012|End:|Thursday, August 16, 2012|Location:|St Louis|Who:|Aleisha Ritter|undertkr2002@yahoo.com|Description:|
            tempStr = tempContent.Split("|")

            Dim currentHeader As String = ""
            For Each oneString As String In tempStr
                'this will be a header followed by a value
                '   Start:
                '   blah blah
                '   End:
                '   blah blah
                '   etc...
                Select Case oneString.Trim.ToLower
                    Case "start:"
                        currentHeader = oneString.Trim.ToLower
                    Case "end:"
                        currentHeader = oneString.Trim.ToLower
                    Case "location:"
                        currentHeader = oneString.Trim.ToLower
                    Case "who:"
                        currentHeader = oneString.Trim.ToLower
                    Case "description:"
                        currentHeader = "skip me"
                    Case Else
                        'This is a value instead of a header then
                        Select Case currentHeader
                            Case "start:"
                                oneEvent.dt_StartTime = DateTime.Parse(oneString.Trim)
                            Case "end:"
                                oneEvent.dt_EndTime = DateTime.Parse(oneString.Trim)
                            Case "location:"
                                oneEvent.str_Location = oneString.Trim
                                If IsAtHome(oneString) Then oneEvent.AtHome = True
                            Case "who:"
                                oneEvent.int_Attending += 1
                            Case "skip me"
                                'doesn't matter to Halcyon
                            Case Else
                                'doesn't matter to Halcyon
                        End Select
                End Select
            Next
        Next

        Return cal
    End Function

    Public Function Iterate_GMAIL()
        'Because i'm iterating a shared calendar XML, we could also do the same for other providers.
        'Just look within the link string of the calendar to determine provider. If it contains "microsoft.com" or "google.com" etc
        Return Nothing
    End Function
End Module

MainPage.xaml

XML
This is Halcyon's interface that can be displayed from the Raspberry Pi. However, once it is finished, this displaying will be optional. In theory, it will "just work" and settings can be changed from your phone app. Language is XAML
<Page
    x:Class="Halcyon_Alpha.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Halcyon_Alpha"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel>
            <TextBlock TextWrapping="Wrap" Text="Halcyon" VerticalAlignment="Top" Margin="0,25,0,0" TextAlignment="Center" Foreground="#FF035CD0" FontWeight="Bold" FontSize="29.333"/>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  >
                <RadioButton x:Name="radioC" Content="Celsius" GroupName="displaySetting" IsChecked="True" />
                <RadioButton x:Name="radioF" Content="Farenheit" GroupName="displaySetting" FlowDirection="RightToLeft" />
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  Margin="0,25,0,0">
                <TextBlock TextWrapping="Wrap" Text="Ambient Temperature:" VerticalAlignment="Top" Margin="0,0,0,0" TextAlignment="Center"/>
                <TextBlock x:Name="tbTempAmbient" TextWrapping="Wrap" Text="loading..." VerticalAlignment="Top" Margin="25,0,0,0" TextAlignment="Center"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  Margin="0,25,0,0">
                <TextBlock TextWrapping="Wrap" Text="Desired Temperature:" VerticalAlignment="Top" Margin="0,0,0,0" TextAlignment="Center"/>
                <TextBlock x:Name="tbTempSetting" TextWrapping="Wrap" Text="loading..." VerticalAlignment="Top" Margin="25,0,0,0" TextAlignment="Center"/>
            </StackPanel>

            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  Margin="0,25,0,0">
                <TextBlock TextWrapping="Wrap" Text="Status:" VerticalAlignment="Top" Margin="0,0,0,0" TextAlignment="Center"/>
                <TextBlock TextWrapping="Wrap" Text="loading..." x:Name="tbStatus" VerticalAlignment="Top" Margin="25,0,0,0" TextAlignment="Center"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  Margin="0,25,0,0">
                <TextBlock TextWrapping="Wrap" Text="Current Time:" VerticalAlignment="Top" Margin="0,0,0,0" TextAlignment="Center"/>
                <TextBlock x:Name="tbCurrentTime"  TextWrapping="Wrap" Text="loading..." VerticalAlignment="Top" Margin="25,0,0,0" TextAlignment="Center"/>
            </StackPanel>

            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"  Margin="0,25,0,0">
                <TextBlock TextWrapping="Wrap" Text="Force Temp:" VerticalAlignment="Top" Margin="0,5,0,0" TextAlignment="Center"/>
                <TextBox x:Name="txtForce" TextWrapping="Wrap" Text="20" VerticalAlignment="Top" Margin="25,0,0,0" TextAlignment="Center"/>
           <Button x:Name="btnForceTemp" Content="Apply" Margin="25,0,0,0"/>
            </StackPanel>

            <TextBlock x:Name="tbError" TextWrapping="Wrap" Text="." VerticalAlignment="Top" Margin="0,25,0,0" TextAlignment="Center" Foreground="Red"/>
        </StackPanel>
    </Grid>
</Page>

MainPage.xaml.vb

VBScript
This is the code behind the Raspberry Pi interface. This is what loads when the firmware is deployed. This code initiates code from other modules, and updates the interface only. No heavy lifting or logic done here. Very simple. Language is VB.NET
'The MainPage code should initiate code on the other modules, and will also be responsible for handling GUI interactions and updates
''' <summary>
''' -> at the start of a comment denotes PSUEDO CODE, not an actual comments
''' </summary>
Public NotInheritable Class MainPage
    Inherits Page

    Private Sub MainPage_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
        Try
            InitializeHardware() 'make sure our hardware connections work and are started up
            InitializeSoftware() 'do initial sync and start our timers to make the magic happen
        Catch ex As Exception
            stopTimers()
            tbError.Text = "ERROR: " & ex.Message
        End Try
    End Sub

    Public Sub UpdateInterface()
        Select Case radioC.IsChecked
            Case True
                'display Celsius
                tbTempAmbient.Text = TempAmbient_Celsius.ToString
                tbTempSetting.Text = TempSet_Celsius.ToString
            Case False
                'display Farenheit
                tbTempAmbient.Text = TempAmbient_Farenheit.ToString
                tbTempSetting.Text = TempSet_Farenheit.ToString
        End Select

        tbCurrentTime.Text = DateTime.Now.ToString
        If ac_ON Then
            tbStatus.Text = "Cooling"
        ElseIf heat_ON Then
            tbStatus.Text = "Heating"
        Else
            tbStatus.Text = "Everything Off"
        End If
    End Sub

    Private Sub btnForceTemp_Click(sender As Object, e As RoutedEventArgs) Handles btnForceTemp.Click
        Try
            If radioC.IsChecked Then
                TempSet_Celsius = Math.Round(CInt(txtForce.Text.Trim), precision)
            Else
                TempSet_Farenheit = Math.Round(CInt(txtForce.Text.Trim), precision)
            End If
        Catch ex As Exception
            tbError.Text = ex.Message
            txtForce.Text = "20"
        End Try
    End Sub
End Class

Away Events.xml

XML
Here is an example of the XML you get when sharing a Microsoft calendar via a link.
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns:sx="http://www.microsoft.com/schemas/sse" xmlns="http://www.w3.org/2005/Atom">
    <title>Away Events</title>
    <subtitle>Halcyon Away Events Testing</subtitle>
    <link rel="self" href="https://sharing.calendar.live.com/calendar/private/eca6cd71-ae7c-4146-b7c1-a8f9d03de0d2/0ff376e0-b10d-4067-acde-71cd450283db/cid-60e973077a985b49/calendar.xml" type="application/atom+xml" />
    <updated>2015-09-16T20:49:49Z</updated>
    <entry>
        <title>Away - Friday, September 25, 2015 8:00AM(Eastern Standard Time)</title>
        <updated>2015-09-16T20:49:19Z</updated>
        <author>
            <email>gd.ritter@live.com</email>
        </author>
        <content type="html">&lt;table style="font-family:tahoma;font-size:11px"&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Start:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Friday, September 25, 2015 8:00AM (Eastern Standard Time)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;End:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Friday, September 25, 2015 1:30PM (Eastern Standard Time)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Repeat:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Occurs every week on Friday.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Location:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Away&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Who:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td /&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Away&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</content>
    </entry>
    <entry>
        <title>Away - Monday, September 21, 2015 8:00AM(Eastern Standard Time)</title>
        <updated>2015-09-16T20:49:48Z</updated>
        <author>
            <email>gd.ritter@live.com</email>
        </author>
        <content type="html">&lt;table style="font-family:tahoma;font-size:11px"&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Start:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Monday, September 21, 2015 8:00AM (Eastern Standard Time)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;End:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Monday, September 21, 2015 6:30PM (Eastern Standard Time)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Repeat:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Occurs every week on Monday, Tuesday, Wednesday and Thursday.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Location:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Away&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Who:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td /&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="vertical-align:top" align="right"&gt;&lt;b&gt;Description:&lt;/b&gt;&lt;/td&gt;&lt;td style="width:10px" align="right" /&gt;&lt;td&gt;Away&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</content>
    </entry>
    <timezone xmlns="http://calendar.live.com/schemas/sync/calendar"><![CDATA[BEGIN:VTIMEZONE
TZID:Eastern Standard Time
BEGIN:STANDARD
DTSTART:20061029T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETTO:-0500
TZOFFSETFROM:-0400
END:STANDARD
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
TZOFFSETTO:-0500
TZOFFSETFROM:-0400
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20060402T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZOFFSETTO:-0400
TZOFFSETFROM:-0500
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
TZOFFSETTO:-0400
TZOFFSETFROM:-0500
END:DAYLIGHT
END:VTIMEZONE
]]></timezone>
</feed>

Credits

Gary Ritter Jr

Gary Ritter Jr

1 project • 4 followers
Just a casual hobbyist that likes to have fun.
Thanks to Joan.

Comments