Saturday, April 11, 2015

Building a SolarEdge Windows Desktop Widget - Part 3

When I first started down this path I had decided that a C# program just wasn't going to be feasible due to the lack of a function to keep it at bottommost (or, in other words, on the desktop).  This in spite of the fact that as far as writing windows programs goes, C# is definitely the language I know the most about.  After the issues I had with the gadgets I decided to take another look at making a C# implementation work.

I started looking around Google to see if I couldn't find something that would work.  I finally settled on the 2nd sub answer of the accepted answer on this stackoverflow http://stackoverflow.com/questions/2027536/setting-a-windows-form-to-be-bottommost.  I also combined that method with a few events that any time the form tried to move forward, it would be sent back.  Essentially this keeps it at the bottom no matter what.

There were a few essential pieces.  In the main class:



[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);

Then we need to override the WndProc method. The key here being the SWP_NOZORDER and SWP_NOOWNERZORDER. Basically we don't want it to change the z-index of the form.



        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
        protected override void WndProc(ref Message m)
        {
            const short SWP_NOZORDER = 0X0004;
            const short SWP_NOACTIVATE = 0X0010;
            const short SWP_NOOWNERZORDER = 0x0200;
            // Listen for operating system messages. 
            switch (m.Msg)
            {
                case WM_WINDOWPOSCHANGING:
                    SetWindowPos(this.Handle, 0, this.Left, this.Top, this.Width, this.Height, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER);

                    // Invalidate to get new text painted. 
                    this.Invalidate();

                    break;

            }
            base.WndProc(ref m);
        }

Once I had taken care of keeping the window on the bottom, the rest of the work was relatively straightforward. This was largely in part due to the fact that I was able to re-use the existing data fetching library I had written for the gadget. (Though I no longer run it in a separate program, it is now handled internally)  At the moment there are 3 main areas of the window.

The first area is the right most area which gives simple current metrics for current power generation in Watts (or Kilowatts usually), then power generated today, this month, this year, and all time (in kWh).  This is all coded with simple labels and set via the .Text property:


lblCurrentPower.Text = data.overviewData.currentPower;
lblCurrentMonth.Text = data.overviewData.lastMonthEnergy + " kWh";
lblLifetimePower.Text = data.overviewData.lifeTimeEnergy + " kWh";
lblTodayPower.Text = data.overviewData.lastDayEnergy + " kWh";
lblLastUpdatedValue.Text = DateTime.Now.ToString();

The second area of the window is the Week view. This is a graph that gives the point in time power generation metrics (in Watts). For this I leveraged the standard windows forms graphing capabilities. Mostly I used the design view to setup the graph, but a few of them I setup in code. One thing I found is that if you put a null after the datapoint you end up with it using whatever the last value was as the next data point. This results in having it show you generating this constant amount of power, when that isn't really the case. To combat this I check for null and do 0 instead if null is found.

            chartWeek.Series[0].XValueType = ChartValueType.DateTime;
            chartWeek.ChartAreas[0].AxisX.LabelStyle.Format = "MM-dd";
            chartWeek.ChartAreas[0].AxisX.Interval = 1;
            chartWeek.ChartAreas[0].AxisX.IntervalType = DateTimeIntervalType.Days;
            chartWeek.ChartAreas[0].AxisX.IntervalOffset = 1;
And when setting the data

            chartWeek.ChartAreas[0].AxisX.Minimum = new DateTime(1970, 1, 1).AddMilliseconds(data.energyChartData.start_week).ToOADate();
            chartWeek.ChartAreas[0].AxisX.Maximum = new DateTime(1970, 1, 1).AddMilliseconds(data.energyChartData.start_week + (data.energyChartData.power_chart_week.Count * 1000 * 60 * 15)).ToOADate();
            chartWeek.Series[0].Points.Clear();

            for (var i = 0; i < data.energyChartData.power_chart_week.Count; i++)
            {
                chartWeek.Series[0].Points.AddXY(new DateTime(1970, 1, 1).AddMilliseconds(data.energyChartData.start_week + (i * 1000 * 60 * 15)), data.energyChartData.power_chart_week[i] == null ? 0 : data.energyChartData.power_chart_week[i]);
            }

I also found and modified some code to do mouse-over tooltips (link). The modification primarily stems from needing to make sure the tooltip wasn't getting re-drawn every time the mouse moved. Normally this isn't a huge issue, but I also customized the tooltip class and when it would redraw you'd end up with this annoying flicker.


void chartmonth_MouseMove(object sender, MouseEventArgs e)
        {
            var pos = e.Location;
            if (prevPosition.HasValue && pos == prevPosition.Value)
                return;

            prevPosition = pos;
            var results = chartMonth.HitTest(pos.X, pos.Y, false,
                                            ChartElementType.DataPoint);
            if (results.Any())
            {
                
                foreach (var result in results)
                {
                    if (result.ChartElementType == ChartElementType.DataPoint)
                    {
                        var prop = result.Object as DataPoint;
                        if (prop != null)
                        {
                            var pointXPixel = result.ChartArea.AxisX.ValueToPixelPosition(prop.XValue);
                            var pointYPixel = result.ChartArea.AxisY.ValueToPixelPosition(prop.YValues[0]);

                            // check if the cursor is really close to the point (2 pixels around the point)
                            if (Math.Abs(pos.X - pointXPixel) < 30 &&
                                Math.Abs(pos.Y - pointYPixel) < 15)
                            {
                                
                                if ((double)tooltip.Tag != prop.YValues[0])
                                {
                                    tooltip.Show(prop.YValues[0] + " kWh", chartMonth,
                                                    pos.X + 15, pos.Y - 15);
                                    tooltip.Tag = prop.YValues[0];
                                }
                            }
                            else
                            {
                                tooltip.RemoveAll();
                                tooltip.Tag = 0.0;
                            }
                        }
                    }else
                    {
                        tooltip.RemoveAll();
                        tooltip.Tag = 0.0;
                    }
                }
            }
            else
            {
                tooltip.RemoveAll();
                tooltip.Tag = 0.0;
            }
        }

The tooltip class looks like this. It was based off of this post, but with vastly better color choices :).

    class CustomToolTip : ToolTip
    {
        public CustomToolTip()
        {
            this.OwnerDraw = true;
            this.Popup += new PopupEventHandler(this.OnPopup);
            this.Draw += new DrawToolTipEventHandler(this.OnDraw);
        }

        private void OnPopup(object sender, PopupEventArgs e) // use this event to set the size of the tool tip
        {
            if ((double)Tag >= 100)
                e.ToolTipSize = new Size(100, 25);
            else
                e.ToolTipSize = new Size(80,25);
            
        }

        private void OnDraw(object sender, DrawToolTipEventArgs e) // use this event to customise the tool tip
        {
            Graphics g = e.Graphics;

            g.FillRectangle(Brushes.Black, e.Bounds);

            g.DrawRectangle(new Pen(new SolidBrush(Color.FromArgb(0, 131, 255)), 1), new Rectangle(e.Bounds.X, e.Bounds.Y,
                e.Bounds.Width - 1, e.Bounds.Height - 1));

            g.DrawString(e.ToolTipText, new Font(e.Font, FontStyle.Bold), Brushes.White, new PointF(e.Bounds.X + 5, e.Bounds.Y + 5)); // top layer
        }
    }

The third and final current piece of the form is the month view. This gives you the daily generation of power (in kWh) for the current month. This uses a standard bar graph and also almost identical tooltips. I don't know why, but the first data point in a bar graph is cut in half. To alter this I changed the minimum of the graph to .5 and offset it by .5.

            chartMonth.ChartAreas[0].AxisX.Minimum = .5;
            chartMonth.ChartAreas[0].AxisX.IntervalOffset = .5;
            chartMonth.ChartAreas[0].AxisX.Interval = 1;
            lblMonthNow.Text = DateTime.Now.ToString("MMMM");
Populating the graph was very similar to the week code:

            chartMonth.Series[0].Points.Clear();

            for (var i = 0; i < data.energyChartData.energy_chart_month_by_day.production.data.Count; i++)
            {
                chartMonth.Series[0].Points.AddXY(i+1, data.energyChartData.energy_chart_month_by_day.production.data[i]);
            }

When all is said and done the results look something like this:



So what's left now? The window is currently hard coded to sit on a specific place on my desktop.  This works, but I would much rather be able to set it dynamically with a simple drag.  The username, password, and site ID for the connection back to solaredge is also hard coded.  Both of these need to be moved into a configuration file for persistence and to reduce the effort to set up the interface.  I'm not 100% happy on the aesthetic designs yet either, but haven't settled on what changes I'd like to make to improve it.  There are also some places in the code that needs cleaning and simplifying.  Lastly I still am considering adding the year view to this as well.  Once I'm happy with the full design I will release the code for anyone to use.

No comments:

Post a Comment