Make it work without MQTT.
4 files changed, 162 insertions(+), 31 deletions(-)

M dash/app.py
M dash/poetry.lock
M dash/pyproject.toml
M dash/templates/main.html
M dash/app.py +49 -25
@@ 8,35 8,61 @@ import json
 
 app = Quart('fronius-dashboard')
 
-BROKER_URL = os.environ.get("BROKER_URL", 'mqtt://mqtt.lan')
+BROKER_URL = os.environ.get("BROKER_URL")
+INVERTER = os.environ.get('INVERTER')
 INVERTER_SIZE = int(os.environ.get('INVERTER_SIZE', 5000))
 TOPIC = os.environ.get('TOPIC', 'solar/current')
 
-BROKER = urlparse(BROKER_URL)
-
 @app.route('/')
 async def main():
     return await render_template('main.html', inverter_size=INVERTER_SIZE)
 
 
-async def messages(topic=TOPIC, broker=BROKER.hostname):
-    async with Client(broker) as client:
-        async with client.filtered_messages(topic) as messages:
-            await client.subscribe(topic)
-            async for message in messages:
-                yield message
-                
+if BROKER_URL:
+    async def messages(topic=TOPIC, broker=urlparse(BROKER_URL).hostname):
+        async with Client(broker) as client:
+            async with client.filtered_messages(topic) as messages:
+                await client.subscribe(topic)
+                async for message in messages:
+                    yield json.loads(message.payload)
+elif INVERTER:
+    import httpx
+    
+    async def messages(inverter=INVERTER):
+        async with httpx.AsyncClient() as client:
+            while True:
+                response = await client.get(f'http://{inverter}/solar_api/v1/GetPowerFlowRealtimeData.fcgi')
+                result = response.json()['Body']['Data']['Site']
+                yield {
+                    'generation': result['P_PV'],
+                    'consumption': -result['P_Load'],
+                    'grid': result['P_Grid'],
+                    'energy': result['E_Day'],
+                }
+            
 
 @app.websocket('/ws')
 async def ws():
     while True:
-        async for message in messages():
-            await websocket.send_json(json.loads(message.payload))
+        try:
+            async for message in messages():
+                await websocket.send_json(message)
+        except Exception:
+            await asyncio.sleep(1)
         
 
 
 class ServerSentEvent:
-
+    """
+    A ServerSentEvent must be in a pretty specific format.
+    
+    data: <data>\n
+    event: <event>\n
+    id: <id>\n
+    retry: <integer>\r\n\r\n
+    
+    Of these, strictly only the data is required.
+    """
     def __init__(
             self,
             data: str,

          
@@ 57,26 83,24 @@ class ServerSentEvent:
             if getattr(self, key) is not None
         ) + '\r\n\r\n'
         return message.encode('utf-8')
-        
-        
+
+
+
 @app.route('/stream')
 async def stream():
-    async def messages(topic):
+    async def _messages():
         while True:
             try:
-                async with Client(BROKER.hostname) as client:
-                    async with client.filtered_messages(topic) as messages:
-                        await client.subscribe(topic)
-                        async for message in messages:
-                            yield ServerSentEvent(
-                                message.payload.decode('utf-8'),
-                                event='update'
-                            ).encode()
+                async for message in messages():
+                    yield ServerSentEvent(
+                        json.dumps(message),
+                        event='update'
+                    ).encode()
             except Exception:
                 await asyncio.sleep(1)
 
     response = await make_response(
-        messages(TOPIC), 
+        _messages(), 
         {
             'Content-Type': 'text/event-stream',
             'Cache-Control': 'no-cache',

          
M dash/poetry.lock +96 -1
@@ 26,6 26,14 @@ optional = false
 python-versions = "*"
 
 [[package]]
+name = "certifi"
+version = "2020.12.5"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
 name = "click"
 version = "7.1.2"
 description = "Composable command line interface toolkit"

          
@@ 62,6 70,39 @@ optional = false
 python-versions = ">=3.6.1"
 
 [[package]]
+name = "httpcore"
+version = "0.12.3"
+description = "A minimal low-level HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+h11 = "<1.0.0"
+sniffio = ">=1.0.0,<2.0.0"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+
+[[package]]
+name = "httpx"
+version = "0.16.1"
+description = "The next generation HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+certifi = "*"
+httpcore = ">=0.12.0,<0.13.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotlipy (>=0.7.0,<0.8.0)"]
+http2 = ["h2 (>=3.0.0,<4.0.0)"]
+
+[[package]]
 name = "hypercorn"
 version = "0.11.2"
 description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn."

          
@@ 91,6 132,14 @@ optional = false
 python-versions = ">=3.6.1"
 
 [[package]]
+name = "idna"
+version = "3.1"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.4"
+
+[[package]]
 name = "itsdangerous"
 version = "1.1.0"
 description = "Various helpers to pass data to untrusted environments and back."

          
@@ 161,6 210,28 @@ werkzeug = ">=1.0.0"
 dotenv = ["python-dotenv"]
 
 [[package]]
+name = "rfc3986"
+version = "1.4.0"
+description = "Validating URI References per RFC 3986"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "sniffio"
+version = "1.2.0"
+description = "Sniff out which async library your code is running under"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
 name = "toml"
 version = "0.10.2"
 description = "Python Library for Tom's Obvious, Minimal Language"

          
@@ 194,7 265,7 @@ h11 = ">=0.9.0,<1"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.9"
-content-hash = "28ee4dcfa98ff0b84713103310b782d79d95a0e8034aa34b86583957b372e1bb"
+content-hash = "92c8f3170c3b980c8c880c6b4f535c9a808fd83d6538d4c1253e4704c92b35ba"
 
 [metadata.files]
 aiofiles = [

          
@@ 208,6 279,10 @@ asyncio-mqtt = [
 blinker = [
     {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"},
 ]
+certifi = [
+    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+]
 click = [
     {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
     {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},

          
@@ 224,6 299,14 @@ hpack = [
     {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
     {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
 ]
+httpcore = [
+    {file = "httpcore-0.12.3-py3-none-any.whl", hash = "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"},
+    {file = "httpcore-0.12.3.tar.gz", hash = "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9"},
+]
+httpx = [
+    {file = "httpx-0.16.1-py3-none-any.whl", hash = "sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"},
+    {file = "httpx-0.16.1.tar.gz", hash = "sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537"},
+]
 hypercorn = [
     {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"},
     {file = "Hypercorn-0.11.2.tar.gz", hash = "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a"},

          
@@ 232,6 315,10 @@ hyperframe = [
     {file = "hyperframe-6.0.0-py3-none-any.whl", hash = "sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"},
     {file = "hyperframe-6.0.0.tar.gz", hash = "sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1"},
 ]
+idna = [
+    {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"},
+    {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"},
+]
 itsdangerous = [
     {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
     {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},

          
@@ 286,6 373,14 @@ quart = [
     {file = "Quart-0.14.1-py3-none-any.whl", hash = "sha256:7b13786e07541cc9ce1466fdc6a6ccd5f36eb39118edd25a42d617593cd17707"},
     {file = "Quart-0.14.1.tar.gz", hash = "sha256:429c5b4ff27e1d2f9ca0aacc38f6aba0ff49b38b815448bf24b613d3de12ea02"},
 ]
+rfc3986 = [
+    {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"},
+    {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"},
+]
+sniffio = [
+    {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
+    {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
+]
 toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},

          
M dash/pyproject.toml +1 -0
@@ 9,6 9,7 @@ python = "^3.9"
 paho-mqtt = "^1.5.0"
 Quart = "^0.14.1"
 asyncio-mqtt = "^0.8.0"
+httpx = "^0.16.1"
 
 [tool.poetry.dev-dependencies]
 

          
M dash/templates/main.html +16 -5
@@ 20,7 20,7 @@ 
   <body >
     <div>
     <div class="main"
-         data-bind="class: generating ? 'on' : 'off'">
+         data-bind="class: generating() ? 'on' : 'off'">
       <svg xmlns="http://www.w3.org/2000/svg"
            height="100%"
            viewBox="0 0 200 200"

          
@@ 95,7 95,8 @@ 
         generation: ko.observable(0),
         consumption: ko.observable(0),
         grid: ko.observable(0),
-        energy: ko.observable(0)
+        energy: ko.observable(0),
+        last_seen_generating: ko.observable()
       };
       
       viewModel.update = function(data) {

          
@@ 103,9 104,12 @@ 
         this.consumption(parseInt(data.consumption));
         this.grid(parseInt(data.grid));
         this.energy(parseInt(data.energy));
+        if (this.generation() > 0) {
+          this.last_seen_generating(new Date());
+        }
       }.bind(viewModel);
       
-      viewModel.generating = ko.computed(() => true);
+      viewModel.generating = ko.computed(() => new Date() - viewModel.last_seen_generating() < 300000);
       viewModel.generation.percentage = ko.computed(
         () => viewModel.generation() > inverter_size ? 
           100 : (100 * viewModel.generation() / inverter_size).toFixed(0)

          
@@ 144,10 148,17 @@ 
       // websocket.onmessage = function(event) {
       //   viewModel.update(JSON.parse(event.data));
       // }
-            
+      
+      window.viewModel = viewModel;
+      
       ko.applyBindings(viewModel);
       
-      
+      const toggleFullScreen = () => {
+        document.webkitCurrentFullScreenElement ?   
+              document.webkitExitFullscreen() :   
+              document.documentElement.webkitRequestFullScreen();
+      };
+      window.addEventListener('click', toggleFullScreen);
     </script>
   </body>
 </html>