Current status of BPUP

Looking at implementing BPUP in my ISY 994i integration (Polyglot Nodeserver). Have there been any changes to BPUP since the documentation in the API? Also, what is the rationale behind having BPUP send HTTP information (status code, method, etc.)? Wouldn’t just a very simple datagram suffice (device id, status info).

I hope lack of reply doesn’t mean BPUP is dead.

I guess my question is answered.

@SDamiano - Sara I believe you and @merck mentioned you were successfully using BPUP in your OpenHAB integration, no?

Do you have any pointers or notes you’d be willing to share with @kingwr?

I’m using BPUP in my HomeSeer integration, works fine.

Below is my C# class. (Couldn’t find how to attach the file)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using System.Net.Sockets;
using System.Net;
using Newtonsoft.Json;


namespace HSPI_AKBond
{
    using State = Dictionary<string, object>;

    /// <summary>
    /// Bond Push UDP Protocol (BPUP)
    /// </summary>
    public class BPUP
    {
        /// <summary>
        /// Reply received for UDP broadcast
        /// </summary>
        class BPUP_MSG
        {
            // the Bond ID
            public string B { get; set; }
            // topic (the path from HTTP URL)
            public string t { get; set; }
            // request ID
            public string i { get; set; }
            // flags (Olibra-internal use)
            public int f { get; set; }
            // HTTP status code
            public int s { get; set; }
            // HTTP method (0=GET, 1=POST, 2=PUT, 3=DELETE, 4=PATCH)
            public int m { get; set; }
            // HTTP response body
            public State b { get; set; }
        }


        /// <summary>
        /// UDP client for Bond Push UDP Protocol
        /// </summary>
        UdpClient udp;

        /// <summary>
        /// IPEndPoint for UdpClient
        /// </summary>
        IPEndPoint udp_ep;

        /// <summary>
        /// CancellationToken for UDPListenTask
        /// </summary>
        CancellationTokenSource cts;

        /// <summary>
        /// Timer for sening KeepAlive for BPUP (60 sec)
        /// </summary>
        Timer KeepAliveTimer;


        DeviceBondBridge bridge;


        public bool connected
        { 
            get => _connected;

            private set
            {
                if (bridge != null)
                    bridge.BPUP_Connected(value, value != _connected);

                _connected = value;
            }
        }

        bool _connected;


        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="bridge"></param>
        public BPUP(DeviceBondBridge bridge)
        {
            this.bridge = bridge;
        }


        void LogErrPBUP(string err, Exception e)
        {
            bridge.LogErr($"{err} {e}");

            connected = false;
        }


        /// <summary>
        /// To ensure when receiving task dies - restart it if PBUP is supposed to be running
        /// And don't restart if not supposed
        /// </summary>
        bool should_run;


        public void End(bool set_internal = true)
        {
            if (set_internal)
                should_run = false;

            if (KeepAliveTimer != null)
            {
                KeepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite);
                KeepAliveTimer.Dispose();
                KeepAliveTimer = null;
            }

            if (cts != null)
                cts.Cancel();

            if (udp != null)
                udp.Close();
        }


        /// <summary>
        /// Create UDP client for Bond Push UDP Protocol
        /// </summary>
        /// <returns></returns>
        public bool ConnectUDP(bool set_internal = true)
        {
            if (set_internal)
                should_run = true;

            try
            {
                End(set_internal: false);

                udp_ep = new IPEndPoint(bridge.receiver?.IPEndPoint?.Address, 30007);
                bridge.Log($"ConnectUDP: {udp_ep}");

                udp = new UdpClient();
                udp.Connect(udp_ep);

                StartUDPListen();

                KeepAliveTimer = new Timer(SendKeepAliveTimerCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));

                return true;
            }
            catch (Exception e)
            {
                LogErrPBUP("ConnectUDP error", e);
                return false;
            }
        }


        /// <summary>
        /// Timer callback
        /// </summary>
        /// <param name="state"></param>
        void SendKeepAliveTimerCallback(object state)
        {
            // If timer fired, but request_sent wasn't reset - means reply wasn't received
            if (request_sent)
            {
                connected = false;
                //ConnectUDP();
                //return;
            }

            if(!connected || (DateTime.Now - lastKeepAlive).TotalSeconds > KeepAliveIntrvl)
                SendKeepAlive();
        }


        DateTime lastKeepAlive = DateTime.Now;

        #if DEBUG
        int KeepAliveIntrvl = 15; // sec
        #else
        int KeepAliveIntrvl = 60; // sec
        #endif

        /// <summary>
        /// Send KeepAlive UDP message to Bridge
        /// </summary>
        /// <returns></returns>
        bool SendKeepAlive()
        {
            try
            {
                bridge.Log($"*** SendKeepAlive ***");

                if (udp?.Client == null || !udp.Client.Connected)
                    return ConnectUDP(set_internal: false);

                // Send '\n' udp to Bridge
                int n = udp.Send(kad, kad.Length);

                request_sent = true;

                // Don't need to recieve - because StartUDPListen() was already started
                return connected;
            }
            catch (Exception e)
            {
                LogErrPBUP("SendKeepAlive error", e);
                return false;
            }
        }

        /// <summary>
        /// KeepAlive msg
        /// </summary>
        readonly byte[] kad = new byte[] { (byte)'\n' };

        /// <summary>
        /// 
        /// </summary>
        bool request_sent;


        /// <summary>
        /// Start UDPListenTask 
        /// </summary>
        private void StartUDPListen()
        {
            // Just in case?
            if (cts != null)
                cts.Cancel();

            cts = new CancellationTokenSource();

            Task.Factory.StartNew(() => { UDPListenTask(cts.Token); }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }


        /// <summary>
        /// Listen UDP socket
        /// </summary>
        /// <param name="token"></param>
        private void UDPListenTask(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                BPUP_MSG msg = ReceiveUDP<BPUP_MSG>();

                if (token.IsCancellationRequested)
                {
                    connected = false;
                    break;
                }
                else
                {
                    if (CheckReply(msg))
                        ProcessReply(msg);
                }
            }

            bridge.LogWarn($"Finished UDPListenTask");

            if (should_run)
                ConnectUDP(set_internal: false);
        }


        /// <summary>
        /// Receive from udp client
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        T ReceiveUDP<T>()
        {
            byte[] receivedBytes = null;
            try
            {
                receivedBytes = udp.Receive(ref udp_ep);
            }
            catch (Exception e)
            {
                bridge.LogWarn($"udp.Receive: {e}");
            }

            if (receivedBytes == null || receivedBytes.Length == 0)
                return default(T);

            string json = Encoding.Default.GetString(receivedBytes);

            bridge.Log($"BPUP status update: {json}");

            try
            {
                return JsonConvert.DeserializeObject<T>(json);
            }
            catch (Exception e)
            {
                bridge.LogWarn($"Deserialising UDP '{json}': {e}");
                return default(T);
            }
        }


        /// <summary>
        /// Check if correct reply received
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        bool CheckReply(BPUP_MSG msg)
        {
            if (msg == null)
            {
                bridge.LogWarn("ReceiveUDP returned null");
                connected = false;
                // TEMP - TODO: need to re-create udp?
                return false;
            }

            if (msg.B != bridge.id)
            {
                bridge.LogWarn($"ReceiveUDP received id '{msg.B}', but expected '{bridge.id}'");
                return false;
            }

            // All good - reset everything and store current time
            if (request_sent && connected)
                lastKeepAlive = DateTime.Now;

            request_sent = false;
            connected = true;

            return true;
        }


        /// <summary>
        /// Process received Reply
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        bool ProcessReply(BPUP_MSG msg)
        {
            // Extract device ID from msg topic and find the device
            string devid = GetDeviceID(msg.t);

            if (String.IsNullOrEmpty(devid))
                return false;

            DeviceBondDevice dev = bridge.FindDevice(devid);

            if (dev == null)
            {
                bridge.LogWarn($"ReceiveUDP device id not found {devid} ({msg.t})");
                return false;
            }

            // Finally pass new status to the device
            //Log($"BPUP status update: {msg.b}");
            dev.state = msg.b;

            return true;
        }


        /// <summary>
        /// i.e. "devices/{id}/state"
        /// </summary>
        Regex rx = new Regex(@"devices\/(.+)\/state", RegexOptions.Compiled | RegexOptions.IgnoreCase);


        /// <summary>
        /// Extract updated device id from topic 
        /// </summary>
        /// <param name="topic">i.e. "devices/{id}/state"</param>
        /// <returns></returns>
        string GetDeviceID(string topic)
        {
            if (String.IsNullOrEmpty(topic))
            {
                // Empty topic is received for KeepAlive message
                //LogWarn($"ReceiveUDP received empty topic '{topic}'");
                return null;
            }

            GroupCollection matches = rx.Match(topic).Groups;

            if (matches.Count != 2)
            {
                bridge.LogWarn($"ReceiveUDP unexpected received topic '{topic}'");
                return null;
            }

            return matches[1].Value;
        }
    }
}
1 Like