WeatherHat Settings

I live in the US. Is there a way for the weatherhat sensors to display imperial readings?

With added conversion code yes. And a few cosmetic edits.
I live in Canada, and once apron a time I had to do it the other way.
For you it should be f=c*1.8+32

I tried. Maybe i didn’t do it right.

Post your code and I’ll be happy to have a look see.
Click the </> preformatted text option to wrap it in code tags.
That will retain indents / formatting etc.

Looks very complicated.

code from weather.py

#!/usr/bin/env python3
import math
import pathlib
import time

import RPi.GPIO as GPIO
import ST7789
import yaml
from fonts.ttf import ManropeBold as UserFont
from PIL import Image, ImageDraw, ImageFont

import weatherhat
from weatherhat import history

FPS = 10

BUTTONS = [5, 6, 16, 24]
LABELS = [“A”, “B”, “X”, “Y”]

DISPLAY_WIDTH = 240
DISPLAY_HEIGHT = 240
SPI_SPEED_MHZ = 80

COLOR_WHITE = (255, 255, 255)
COLOR_BLUE = (31, 137, 251)
COLOR_GREEN = (99, 255, 124)
COLOR_YELLOW = (254, 219, 82)
COLOR_RED = (247, 0, 63)
COLOR_BLACK = (0, 0, 0)
COLOR_GREY = (100, 100, 100)

We can compensate for the heat of the Pi and other environmental conditions using a simple offset.

Change this number to adjust temperature compensation!

OFFSET = -14.5

class View:
def init(self, image):
self._image = image
self._draw = ImageDraw.Draw(image)

    self.font_large = ImageFont.truetype(UserFont, 80)
    self.font = ImageFont.truetype(UserFont, 50)
    self.font_medium = ImageFont.truetype(UserFont, 44)
    self.font_small = ImageFont.truetype(UserFont, 28)

@property
def canvas_width(self):
    return self._image.size[0]

@property
def canvas_height(self):
    return self._image.size[1]

def button_a(self):
    return False

def button_b(self):
    return False

def button_x(self):
    return False

def button_y(self):
    return False

def update(self):
    pass

def render(self):
    self.clear()

def clear(self):
    self._draw.rectangle((0, 0, self.canvas_width, self.canvas_height), (0, 0, 0))

class SensorView(View):
title = “”
GRAPH_BAR_WIDTH = 20

def __init__(self, image, sensordata, settings=None):
    View.__init__(self, image)
    self._data = sensordata
    self._settings = settings

def blend(self, a, b, factor):
    blend_b = factor
    blend_a = 1.0 - factor
    return tuple([int((a[i] * blend_a) + (b[i] * blend_b)) for i in range(3)])

def heading(self, data, units):
    if data < 100:
        data = "{:0.1f}".format(data)
    else:
        data = "{:0.0f}".format(data)

    tw, th = self._draw.textsize(data, self.font_large)

    self._draw.text(
        (0, 32),
        data,
        font=self.font_large,
        fill=COLOR_WHITE,
        anchor="lm"
    )

    self._draw.text(
        (tw, 64),
        units,
        font=self.font_medium,
        fill=COLOR_WHITE,
        anchor="lb"
    )

def footer(self, label):
    self._draw.text((int(self.canvas_width / 2), self.canvas_height - 30), label, font=self.font_medium, fill=COLOR_GREY, anchor="mm")

def graph(self, values, graph_x=0, graph_y=0, width=None, height=None, vmin=0, vmax=1.0, bar_width=2, colors=None):
    if not len(values):
        return

    if width is None:
        width = self.canvas_width

    if height is None:
        height = self.canvas_height

    if colors is None:
        #         Blue          Teal           Green        Yellow         Red
        colors = [(0, 0, 255), (0, 255, 255), (0, 255, 0), (255, 255, 0), (255, 0, 0)]

    vrange = vmax - vmin
    vstep = float(height) / vrange

    if vmin >= 0:
        midpoint_y = height
    else:
        midpoint_y = vmax * vstep
        self._draw.line((graph_x, graph_y + midpoint_y, graph_x + width, graph_y + midpoint_y), fill=COLOR_GREY)

    max_values = int(width / bar_width)

    values = [entry.value for entry in values[-max_values:]]

    for i, v in enumerate(values):
        v = min(vmax, max(vmin, v))

        offset_y = graph_y

        if vmin < 0:
            bar_height = midpoint_y * float(v) / float(vmax)
        else:
            bar_height = midpoint_y * float(v - vmin) / float(vmax - vmin)

        if v < 0:
            offset_y += midpoint_y
            bar_height = (height - midpoint_y) * float(abs(v)) / abs(vmin)

        color = float(v - vmin) / float(vmax - vmin) * (len(colors) - 1)
        color_idx = int(color)      # The integer part of color becomes our index into the colors array
        blend = color - color_idx   # The fractional part forms the blend amount between the two colours
        bar_color = colors[color_idx]
        if color_idx < len(colors) - 1:
            bar_color = self.blend(colors[color_idx], colors[color_idx + 1], blend)
            bar_color = bar_color

        x = (i * bar_width)

        if v < 0:
            self._draw.rectangle((
                graph_x + x, offset_y,
                graph_x + x + int(bar_width / 2), offset_y + bar_height
            ), fill=bar_color)
        else:
            self._draw.rectangle((
                graph_x + x, offset_y + midpoint_y - bar_height,
                graph_x + x + int(bar_width / 2), offset_y + midpoint_y
            ), fill=bar_color)

class MainView(SensorView):
“”"Main Overview.

Displays weather summary and navigation hints.

"""

title = "Overview"

def draw_info(self, x, y, color, label, data, desc, right=False, vmin=0, vmax=20, graph_mode=False):
    w = 200
    o_x = 0 if right else 40

    if graph_mode:
        vmax = max(vmax, max([h.value for h in data]))  # auto ranging?
        self.graph(data, x + o_x + 30, y + 20, 180, 64, vmin=vmin, vmax=vmax, bar_width=20, colors=[color])
    else:
        if type(data) is list:
            if len(data) > 0:
                data = data[-1].value
            else:
                data = 0

        if data < 100:
            data = "{:0.1f}".format(data)
        else:
            data = "{:0.0f}".format(data)

        self._draw.text(
            (x + w + o_x, y + 20 + 32),  # Position is the right, center of the text
            data,
            font=self.font_large,
            fill=color,
            anchor="rm"  # Using "rm" stops text jumping vertically
        )

    self._draw.text(
        (x + w + o_x, y + 90 + 40),
        desc,
        font=self.font,
        fill=COLOR_WHITE,
        anchor="rb"
    )
    label_img = Image.new("RGB", (130, 40))
    label_draw = ImageDraw.Draw(label_img)
    label_draw.text((0, 40) if right else (0, 0), label, font=self.font_medium, fill=COLOR_GREY, anchor="lb" if right else "lt")
    label_img = label_img.rotate(90, expand=True)
    if right:
        self._image.paste(label_img, (x + w, y))
    else:
        self._image.paste(label_img, (x, y))

def render(self):
    SensorView.render(self)
    self.render_graphs()

def render_graphs(self, graph_mode=False):
    self.draw_info(0, 0, (20, 20, 220), "RAIN", self._data.rain_mm_sec.history(), "mm/s", vmax=self._settings.maximum_rain_mm, graph_mode=graph_mode)
    self.draw_info(0, 150, (20, 20, 220), "PRES", self._data.pressure.history(), "hPa", graph_mode=graph_mode)
    self.draw_info(0, 300, (20, 100, 220), "TEMP", self._data.temperature.history(), "°F", graph_mode=graph_mode, vmin=self._settings.minimum_temperature, vmax=self._settings.maximum_temperature)

    x = int(self.canvas_width / 2)
    self.draw_info(x, 0, (220, 20, 220), "WIND", self._data.wind_speed.history(), "m/s", right=True, graph_mode=graph_mode)
    self.draw_info(x, 150, (220, 100, 20), "LIGHT", self._data.lux.history(), "lux", right=True, graph_mode=graph_mode)
    self.draw_info(x, 300, (10, 10, 220), "HUM", self._data.relative_humidity.history(), "%rh", right=True, graph_mode=graph_mode)

class MainViewGraph(MainView):
title = “Overview: Graphs”

def render(self):
    SensorView.render(self)
    self.render_graphs(graph_mode=True)

class WindDirectionView(SensorView):
“”“Wind Direction.”“”

title = "Wind"
metric = "m/sec"

def __init__(self, image, sensordata, settings=None):
    SensorView.__init__(self, image, sensordata, settings)

def render(self):
    SensorView.render(self)
    ox = self.canvas_width / 2
    oy = 40 + ((self.canvas_height - 60) / 2)
    needle = self._data.needle
    speed_ms = self._data.wind_speed.average(60)
    # gust_ms = self._data.wind_speed.gust()
    compass_direction = self._data.wind_direction.average_compass()

    radius = 80
    speed_max = 4.4  # m/s
    speed = min(speed_ms, speed_max)
    speed /= float(speed_max)

    arrow_radius_min = 20
    arrow_radius_max = 60
    arrow_radius = (speed * (arrow_radius_max - arrow_radius_min)) + arrow_radius_min
    arrow_angle = math.radians(130)

    tx, ty = ox + math.sin(needle) * (radius - arrow_radius), oy - math.cos(needle) * (radius - arrow_radius)
    ax, ay = ox + math.sin(needle) * (radius - arrow_radius), oy - math.cos(needle) * (radius - arrow_radius)

    arrow_xy_a = ax + math.sin(needle - arrow_angle) * arrow_radius, ay - math.cos(needle - arrow_angle) * arrow_radius
    arrow_xy_b = ax + math.sin(needle) * arrow_radius, ay - math.cos(needle) * arrow_radius
    arrow_xy_c = ax + math.sin(needle + arrow_angle) * arrow_radius, ay - math.cos(needle + arrow_angle) * arrow_radius

    # Compass red end
    self._draw.line((
        ox,
        oy,
        tx,
        ty
    ), (255, 0, 0), 5)

    # Compass white end
    """
    self._draw.line((
        ox,
        oy,
        ox + math.sin(needle - math.pi) * radius,
        oy - math.cos(needle - math.pi) * radius
    ), (255, 255, 255), 5)
    """

    self._draw.polygon([arrow_xy_a, arrow_xy_b, arrow_xy_c], fill=(255, 0, 0))

    if self._settings.wind_trails:
        trails = 40
        trail_length = len(self._data.needle_trail)
        for i, p in enumerate(self._data.needle_trail):
            # r = radius
            r = radius + trails - (float(i) / trail_length * trails)
            x = ox + math.sin(p) * r
            y = oy - math.cos(p) * r

            self._draw.ellipse((x - 2, y - 2, x + 2, y + 2), (int(255 / trail_length * i), 0, 0))

    radius += 60
    for direction, name in weatherhat.wind_degrees_to_cardinal.items():
        p = math.radians(direction)
        x = ox + math.sin(p) * radius
        y = oy - math.cos(p) * radius

        name = "".join([word[0] for word in name.split(" ")])
        tw, th = self._draw.textsize(name, font=self.font_small)
        x -= tw / 2
        y -= th / 2
        self._draw.text((x, y), name, font=self.font_small, fill=COLOR_GREY)

    self.heading(speed_ms, self.metric)
    self.footer(self.title.upper())

    direction_text = "".join([word[0] for word in compass_direction.split(" ")])

    self._draw.text(
        (self.canvas_width, 32),
        direction_text,
        font=self.font_large,
        fill=COLOR_WHITE,
        anchor="rm"
    )

class WindSpeedView(SensorView):
“”“Wind Speed.”“”

title = "WIND"
metric = "m/s"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.wind_speed.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.wind_speed.history(),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=self._settings.minimum_wind_ms,
        vmax=self._settings.maximum_wind_ms,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class RainView(SensorView):
“”“Rain.”“”

title = "Rain"
metric = "mm/s"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.rain_mm_sec.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.rain_mm_sec.history(),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=self._settings.minimum_rain_mm,
        vmax=self._settings.maximum_rain_mm,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class TemperatureView(SensorView):
“”“Temperature.”“”

title = "TEMP"
metric = "C"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.temperature.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.temperature.history(),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=self._settings.minimum_temperature,
        vmax=self._settings.maximum_temperature,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class LightView(SensorView):
“”“Light.”“”

title = "Light"
metric = "lux"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.lux.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.lux.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=self._settings.minimum_lux,
        vmax=self._settings.maximum_lux,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class PressureView(SensorView):
“”“Pressure.”“”

title = "PRESSURE"
metric = "hPa"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.pressure.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.pressure.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=self._settings.minimum_pressure,
        vmax=self._settings.maximum_pressure,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class HumidityView(SensorView):
“”“Pressure.”“”

title = "Humidity"
metric = "%rh"

def render(self):
    SensorView.render(self)
    self.heading(
        self._data.relative_humidity.latest().value,
        self.metric
    )
    self.footer(self.title.upper())

    self.graph(
        self._data.relative_humidity.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
        graph_x=4,
        graph_y=70,
        width=self.canvas_width,
        height=self.canvas_height - 130,
        vmin=0,
        vmax=100,
        bar_width=self.GRAPH_BAR_WIDTH
    )

class ViewController:
def init(self, views):
self.views = views
self._current_view = 0
self._current_subview = 0

    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    for pin in BUTTONS:
        GPIO.add_event_detect(pin, GPIO.FALLING, self.handle_button, bouncetime=200)

def handle_button(self, pin):
    index = BUTTONS.index(pin)
    label = LABELS[index]

    if label == "A":  # Select View
        self.button_a()

    if label == "B":
        self.button_b()

    if label == "X":
        self.button_x()

    if label == "Y":
        self.button_y()

@property
def home(self):
    return self._current_view == 0 and self._current_subview == 0

def next_subview(self):
    view = self.views[self._current_view]
    if isinstance(view, tuple):
        self._current_subview += 1
        self._current_subview %= len(view)

def next_view(self):
    self._current_subview = 0
    self._current_view += 1
    self._current_view %= len(self.views)

def prev_view(self):
    self._current_subview = 0
    self._current_view -= 1
    self._current_view %= len(self.views)

def get_current_view(self):
    view = self.views[self._current_view]
    if isinstance(view, tuple):
        view = view[self._current_subview]

    return view

@property
def view(self):
    return self.get_current_view()

def update(self):
    self.view.update()

def render(self):
    self.view.render()

def button_a(self):
    if not self.view.button_a():
        self.next_view()

def button_b(self):
    self.view.button_b()

def button_x(self):
    if not self.view.button_x():
        self.next_subview()
        return True
    return True

def button_y(self):
    return self.view.button_y()

class Config:
“”“Class to hold weather UI settings.”“”
def init(self, settings_file=“settings.yml”):
self._file = pathlib.Path(settings_file)

    self._last_save = None

    # Wind Settings
    self.wind_trails = True

    # BME280 Settings
    self.minimum_temperature = -10
    self.maximum_temperature = 40

    self.minimum_pressure = 1000
    self.maximum_pressure = 1100

    self.minimum_lux = 100
    self.maximum_lux = 1000

    self.minimum_rain_mm = 0
    self.maximum_rain_mm = 10

    self.minimum_wind_ms = 0
    self.maximum_wind_ms = 40

    self.load()

def load(self):
    if not self._file.is_file():
        return False

    try:
        self._config = yaml.safe_load(open(self._file))
    except yaml.parser.ParserError as e:
        raise yaml.parser.ParserError(
            "Error parsing settings file: {} ({})".format(self._file, e)
        )

@property
def _config(self):
    options = {}
    for k, v in self.__dict__.items():
        if not k.startswith("_"):
            options[k] = v
    return options

@_config.setter
def _config(self, config):
    for k, v in self.__dict__.items():
        if k in config:
            setattr(self, k, config[k])

class SensorData:
AVERAGE_SAMPLES = 120
WIND_DIRECTION_AVERAGE_SAMPLES = 60
COMPASS_TRAIL_SIZE = 120

def __init__(self):
    self.sensor = weatherhat.WeatherHAT()

    self.temperature = history.History()

    self.pressure = history.History()

    self.humidity = history.History()
    self.relative_humidity = history.History()
    self.dewpoint = history.History()

    self.lux = history.History()

    self.wind_speed = history.WindSpeedHistory()
    self.wind_direction = history.WindDirectionHistory()

    self.rain_mm_sec = history.History()
    self.rain_total = 0

    # Track previous average values to give the compass a trail
    self.needle_trail = []

def update(self, interval=5.0):
    self.sensor.temperature_offset = OFFSET
    self.sensor.update(interval)

    self.temperature.append(self.sensor.temperature)

    self.pressure.append(self.sensor.pressure)

    self.humidity.append(self.sensor.humidity)
    self.relative_humidity.append(self.sensor.relative_humidity)
    self.dewpoint.append(self.sensor.dewpoint)

    self.lux.append(self.sensor.lux)

    if self.sensor.updated_wind_rain:
        self.rain_total = self.sensor.rain_total
    else:
        self.rain_total = 0

    self.wind_speed.append(self.sensor.wind_speed)
    self.wind_direction.append(self.sensor.wind_direction)

    self.rain_mm_sec.append(self.sensor.rain)

    self.needle = math.radians(self.wind_direction.average(self.WIND_DIRECTION_AVERAGE_SAMPLES))
    self.needle_trail.append(self.needle)
    self.needle_trail = self.needle_trail[-self.COMPASS_TRAIL_SIZE:]

def main():
display = ST7789.ST7789(
rotation=90,
port=0,
cs=1,
dc=9,
backlight=12,
spi_speed_hz=SPI_SPEED_MHZ * 1000 * 1000
)
image = Image.new(“RGBA”, (DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2), color=(255, 255, 255))
sensordata = SensorData()
settings = Config()
viewcontroller = ViewController(
(
(
MainView(image, sensordata, settings),
MainViewGraph(image, sensordata, settings)
),
(
WindDirectionView(image, sensordata, settings),
WindSpeedView(image, sensordata, settings)
),
RainView(image, sensordata, settings),
LightView(image, sensordata, settings),
(
TemperatureView(image, sensordata, settings),
PressureView(image, sensordata, settings),
HumidityView(image, sensordata, settings)
),
)
)

while True:
    sensordata.update(interval=5.0)
    viewcontroller.update()
    viewcontroller.render()
    display.display(image.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT)).convert("RGB"))
    time.sleep(1.0 / FPS)

if name == “main”:
main()

You need to use the Preformatted. Text option, </> for posting code.

Wow, that is a lot of code. I was not expecting it to be that complex. I will have a look see though.

weatherhat-python/examples/weather.py at main · pimoroni/weatherhat-python (github.com)

#!/usr/bin/env python3
import math
import pathlib
import time

import RPi.GPIO as GPIO
import ST7789
import yaml
from fonts.ttf import ManropeBold as UserFont
from PIL import Image, ImageDraw, ImageFont

import weatherhat
from weatherhat import history

FPS = 10

BUTTONS = [5, 6, 16, 24]
LABELS = ["A", "B", "X", "Y"]

DISPLAY_WIDTH = 240
DISPLAY_HEIGHT = 240
SPI_SPEED_MHZ = 80

COLOR_WHITE = (255, 255, 255)
COLOR_BLUE = (31, 137, 251)
COLOR_GREEN = (99, 255, 124)
COLOR_YELLOW = (254, 219, 82)
COLOR_RED = (247, 0, 63)
COLOR_BLACK = (0, 0, 0)
COLOR_GREY = (100, 100, 100)

# We can compensate for the heat of the Pi and other environmental conditions using a simple offset.
# Change this number to adjust temperature compensation!
OFFSET = -7.5


class View:
    def __init__(self, image):
        self._image = image
        self._draw = ImageDraw.Draw(image)

        self.font_large = ImageFont.truetype(UserFont, 80)
        self.font = ImageFont.truetype(UserFont, 50)
        self.font_medium = ImageFont.truetype(UserFont, 44)
        self.font_small = ImageFont.truetype(UserFont, 28)

    @property
    def canvas_width(self):
        return self._image.size[0]

    @property
    def canvas_height(self):
        return self._image.size[1]

    def button_a(self):
        return False

    def button_b(self):
        return False

    def button_x(self):
        return False

    def button_y(self):
        return False

    def update(self):
        pass

    def render(self):
        self.clear()

    def clear(self):
        self._draw.rectangle((0, 0, self.canvas_width, self.canvas_height), (0, 0, 0))


class SensorView(View):
    title = ""
    GRAPH_BAR_WIDTH = 20

    def __init__(self, image, sensordata, settings=None):
        View.__init__(self, image)
        self._data = sensordata
        self._settings = settings

    def blend(self, a, b, factor):
        blend_b = factor
        blend_a = 1.0 - factor
        return tuple([int((a[i] * blend_a) + (b[i] * blend_b)) for i in range(3)])

    def heading(self, data, units):
        if data < 100:
            data = "{:0.1f}".format(data)
        else:
            data = "{:0.0f}".format(data)

        tw, th = self._draw.textsize(data, self.font_large)

        self._draw.text(
            (0, 32),
            data,
            font=self.font_large,
            fill=COLOR_WHITE,
            anchor="lm"
        )

        self._draw.text(
            (tw, 64),
            units,
            font=self.font_medium,
            fill=COLOR_WHITE,
            anchor="lb"
        )

    def footer(self, label):
        self._draw.text((int(self.canvas_width / 2), self.canvas_height - 30), label, font=self.font_medium, fill=COLOR_GREY, anchor="mm")

    def graph(self, values, graph_x=0, graph_y=0, width=None, height=None, vmin=0, vmax=1.0, bar_width=2, colors=None):
        if not len(values):
            return

        if width is None:
            width = self.canvas_width

        if height is None:
            height = self.canvas_height

        if colors is None:
            #         Blue          Teal           Green        Yellow         Red
            colors = [(0, 0, 255), (0, 255, 255), (0, 255, 0), (255, 255, 0), (255, 0, 0)]

        vrange = vmax - vmin
        vstep = float(height) / vrange

        if vmin >= 0:
            midpoint_y = height
        else:
            midpoint_y = vmax * vstep
            self._draw.line((graph_x, graph_y + midpoint_y, graph_x + width, graph_y + midpoint_y), fill=COLOR_GREY)

        max_values = int(width / bar_width)

        values = [entry.value for entry in values[-max_values:]]

        for i, v in enumerate(values):
            v = min(vmax, max(vmin, v))

            offset_y = graph_y

            if vmin < 0:
                bar_height = midpoint_y * float(v) / float(vmax)
            else:
                bar_height = midpoint_y * float(v - vmin) / float(vmax - vmin)

            if v < 0:
                offset_y += midpoint_y
                bar_height = (height - midpoint_y) * float(abs(v)) / abs(vmin)

            color = float(v - vmin) / float(vmax - vmin) * (len(colors) - 1)
            color_idx = int(color)      # The integer part of color becomes our index into the colors array
            blend = color - color_idx   # The fractional part forms the blend amount between the two colours
            bar_color = colors[color_idx]
            if color_idx < len(colors) - 1:
                bar_color = self.blend(colors[color_idx], colors[color_idx + 1], blend)
                bar_color = bar_color

            x = (i * bar_width)

            if v < 0:
                self._draw.rectangle((
                    graph_x + x, offset_y,
                    graph_x + x + int(bar_width / 2), offset_y + bar_height
                ), fill=bar_color)
            else:
                self._draw.rectangle((
                    graph_x + x, offset_y + midpoint_y - bar_height,
                    graph_x + x + int(bar_width / 2), offset_y + midpoint_y
                ), fill=bar_color)


class MainView(SensorView):
    """Main Overview.

    Displays weather summary and navigation hints.

    """

    title = "Overview"

    def draw_info(self, x, y, color, label, data, desc, right=False, vmin=0, vmax=20, graph_mode=False):
        w = 200
        o_x = 0 if right else 40

        if graph_mode:
            vmax = max(vmax, max([h.value for h in data]))  # auto ranging?
            self.graph(data, x + o_x + 30, y + 20, 180, 64, vmin=vmin, vmax=vmax, bar_width=20, colors=[color])
        else:
            if type(data) is list:
                if len(data) > 0:
                    data = data[-1].value
                else:
                    data = 0

            if data < 100:
                data = "{:0.1f}".format(data)
            else:
                data = "{:0.0f}".format(data)

            self._draw.text(
                (x + w + o_x, y + 20 + 32),  # Position is the right, center of the text
                data,
                font=self.font_large,
                fill=color,
                anchor="rm"  # Using "rm" stops text jumping vertically
            )

        self._draw.text(
            (x + w + o_x, y + 90 + 40),
            desc,
            font=self.font,
            fill=COLOR_WHITE,
            anchor="rb"
        )
        label_img = Image.new("RGB", (130, 40))
        label_draw = ImageDraw.Draw(label_img)
        label_draw.text((0, 40) if right else (0, 0), label, font=self.font_medium, fill=COLOR_GREY, anchor="lb" if right else "lt")
        label_img = label_img.rotate(90, expand=True)
        if right:
            self._image.paste(label_img, (x + w, y))
        else:
            self._image.paste(label_img, (x, y))

    def render(self):
        SensorView.render(self)
        self.render_graphs()

    def render_graphs(self, graph_mode=False):
        self.draw_info(0, 0, (20, 20, 220), "RAIN", self._data.rain_mm_sec.history(), "mm/s", vmax=self._settings.maximum_rain_mm, graph_mode=graph_mode)
        self.draw_info(0, 150, (20, 20, 220), "PRES", self._data.pressure.history(), "hPa", graph_mode=graph_mode)
        self.draw_info(0, 300, (20, 100, 220), "TEMP", self._data.temperature.history(), "°C", graph_mode=graph_mode, vmin=self._settings.minimum_temperature, vmax=self._settings.maximum_temperature)

        x = int(self.canvas_width / 2)
        self.draw_info(x, 0, (220, 20, 220), "WIND", self._data.wind_speed.history(), "m/s", right=True, graph_mode=graph_mode)
        self.draw_info(x, 150, (220, 100, 20), "LIGHT", self._data.lux.history(), "lux", right=True, graph_mode=graph_mode)
        self.draw_info(x, 300, (10, 10, 220), "HUM", self._data.relative_humidity.history(), "%rh", right=True, graph_mode=graph_mode)


class MainViewGraph(MainView):
    title = "Overview: Graphs"

    def render(self):
        SensorView.render(self)
        self.render_graphs(graph_mode=True)


class WindDirectionView(SensorView):
    """Wind Direction."""

    title = "Wind"
    metric = "m/sec"

    def __init__(self, image, sensordata, settings=None):
        SensorView.__init__(self, image, sensordata, settings)

    def render(self):
        SensorView.render(self)
        ox = self.canvas_width / 2
        oy = 40 + ((self.canvas_height - 60) / 2)
        needle = self._data.needle
        speed_ms = self._data.wind_speed.average(60)
        # gust_ms = self._data.wind_speed.gust()
        compass_direction = self._data.wind_direction.average_compass()

        radius = 80
        speed_max = 4.4  # m/s
        speed = min(speed_ms, speed_max)
        speed /= float(speed_max)

        arrow_radius_min = 20
        arrow_radius_max = 60
        arrow_radius = (speed * (arrow_radius_max - arrow_radius_min)) + arrow_radius_min
        arrow_angle = math.radians(130)

        tx, ty = ox + math.sin(needle) * (radius - arrow_radius), oy - math.cos(needle) * (radius - arrow_radius)
        ax, ay = ox + math.sin(needle) * (radius - arrow_radius), oy - math.cos(needle) * (radius - arrow_radius)

        arrow_xy_a = ax + math.sin(needle - arrow_angle) * arrow_radius, ay - math.cos(needle - arrow_angle) * arrow_radius
        arrow_xy_b = ax + math.sin(needle) * arrow_radius, ay - math.cos(needle) * arrow_radius
        arrow_xy_c = ax + math.sin(needle + arrow_angle) * arrow_radius, ay - math.cos(needle + arrow_angle) * arrow_radius

        # Compass red end
        self._draw.line((
            ox,
            oy,
            tx,
            ty
        ), (255, 0, 0), 5)

        # Compass white end
        """
        self._draw.line((
            ox,
            oy,
            ox + math.sin(needle - math.pi) * radius,
            oy - math.cos(needle - math.pi) * radius
        ), (255, 255, 255), 5)
        """

        self._draw.polygon([arrow_xy_a, arrow_xy_b, arrow_xy_c], fill=(255, 0, 0))

        if self._settings.wind_trails:
            trails = 40
            trail_length = len(self._data.needle_trail)
            for i, p in enumerate(self._data.needle_trail):
                # r = radius
                r = radius + trails - (float(i) / trail_length * trails)
                x = ox + math.sin(p) * r
                y = oy - math.cos(p) * r

                self._draw.ellipse((x - 2, y - 2, x + 2, y + 2), (int(255 / trail_length * i), 0, 0))

        radius += 60
        for direction, name in weatherhat.wind_degrees_to_cardinal.items():
            p = math.radians(direction)
            x = ox + math.sin(p) * radius
            y = oy - math.cos(p) * radius

            name = "".join([word[0] for word in name.split(" ")])
            tw, th = self._draw.textsize(name, font=self.font_small)
            x -= tw / 2
            y -= th / 2
            self._draw.text((x, y), name, font=self.font_small, fill=COLOR_GREY)

        self.heading(speed_ms, self.metric)
        self.footer(self.title.upper())

        direction_text = "".join([word[0] for word in compass_direction.split(" ")])

        self._draw.text(
            (self.canvas_width, 32),
            direction_text,
            font=self.font_large,
            fill=COLOR_WHITE,
            anchor="rm"
        )


class WindSpeedView(SensorView):
    """Wind Speed."""

    title = "WIND"
    metric = "m/s"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.wind_speed.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.wind_speed.history(),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=self._settings.minimum_wind_ms,
            vmax=self._settings.maximum_wind_ms,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class RainView(SensorView):
    """Rain."""

    title = "Rain"
    metric = "mm/s"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.rain_mm_sec.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.rain_mm_sec.history(),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=self._settings.minimum_rain_mm,
            vmax=self._settings.maximum_rain_mm,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class TemperatureView(SensorView):
    """Temperature."""

    title = "TEMP"
    metric = "°C"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.temperature.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.temperature.history(),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=self._settings.minimum_temperature,
            vmax=self._settings.maximum_temperature,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class LightView(SensorView):
    """Light."""

    title = "Light"
    metric = "lux"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.lux.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.lux.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=self._settings.minimum_lux,
            vmax=self._settings.maximum_lux,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class PressureView(SensorView):
    """Pressure."""

    title = "PRESSURE"
    metric = "hPa"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.pressure.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.pressure.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=self._settings.minimum_pressure,
            vmax=self._settings.maximum_pressure,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class HumidityView(SensorView):
    """Pressure."""

    title = "Humidity"
    metric = "%rh"

    def render(self):
        SensorView.render(self)
        self.heading(
            self._data.relative_humidity.latest().value,
            self.metric
        )
        self.footer(self.title.upper())

        self.graph(
            self._data.relative_humidity.history(int(self.canvas_width / self.GRAPH_BAR_WIDTH)),
            graph_x=4,
            graph_y=70,
            width=self.canvas_width,
            height=self.canvas_height - 130,
            vmin=0,
            vmax=100,
            bar_width=self.GRAPH_BAR_WIDTH
        )


class ViewController:
    def __init__(self, views):
        self.views = views
        self._current_view = 0
        self._current_subview = 0

        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP)

        for pin in BUTTONS:
            GPIO.add_event_detect(pin, GPIO.FALLING, self.handle_button, bouncetime=200)

    def handle_button(self, pin):
        index = BUTTONS.index(pin)
        label = LABELS[index]

        if label == "A":  # Select View
            self.button_a()

        if label == "B":
            self.button_b()

        if label == "X":
            self.button_x()

        if label == "Y":
            self.button_y()

    @property
    def home(self):
        return self._current_view == 0 and self._current_subview == 0

    def next_subview(self):
        view = self.views[self._current_view]
        if isinstance(view, tuple):
            self._current_subview += 1
            self._current_subview %= len(view)

    def next_view(self):
        self._current_subview = 0
        self._current_view += 1
        self._current_view %= len(self.views)

    def prev_view(self):
        self._current_subview = 0
        self._current_view -= 1
        self._current_view %= len(self.views)

    def get_current_view(self):
        view = self.views[self._current_view]
        if isinstance(view, tuple):
            view = view[self._current_subview]

        return view

    @property
    def view(self):
        return self.get_current_view()

    def update(self):
        self.view.update()

    def render(self):
        self.view.render()

    def button_a(self):
        if not self.view.button_a():
            self.next_view()

    def button_b(self):
        self.view.button_b()

    def button_x(self):
        if not self.view.button_x():
            self.next_subview()
            return True
        return True

    def button_y(self):
        return self.view.button_y()


class Config:
    """Class to hold weather UI settings."""
    def __init__(self, settings_file="settings.yml"):
        self._file = pathlib.Path(settings_file)

        self._last_save = None

        # Wind Settings
        self.wind_trails = True

        # BME280 Settings
        self.minimum_temperature = -10
        self.maximum_temperature = 40

        self.minimum_pressure = 1000
        self.maximum_pressure = 1100

        self.minimum_lux = 100
        self.maximum_lux = 1000

        self.minimum_rain_mm = 0
        self.maximum_rain_mm = 10

        self.minimum_wind_ms = 0
        self.maximum_wind_ms = 40

        self.load()

    def load(self):
        if not self._file.is_file():
            return False

        try:
            self._config = yaml.safe_load(open(self._file))
        except yaml.parser.ParserError as e:
            raise yaml.parser.ParserError(
                "Error parsing settings file: {} ({})".format(self._file, e)
            )

    @property
    def _config(self):
        options = {}
        for k, v in self.__dict__.items():
            if not k.startswith("_"):
                options[k] = v
        return options

    @_config.setter
    def _config(self, config):
        for k, v in self.__dict__.items():
            if k in config:
                setattr(self, k, config[k])


class SensorData:
    AVERAGE_SAMPLES = 120
    WIND_DIRECTION_AVERAGE_SAMPLES = 60
    COMPASS_TRAIL_SIZE = 120

    def __init__(self):
        self.sensor = weatherhat.WeatherHAT()

        self.temperature = history.History()

        self.pressure = history.History()

        self.humidity = history.History()
        self.relative_humidity = history.History()
        self.dewpoint = history.History()

        self.lux = history.History()

        self.wind_speed = history.WindSpeedHistory()
        self.wind_direction = history.WindDirectionHistory()

        self.rain_mm_sec = history.History()
        self.rain_total = 0

        # Track previous average values to give the compass a trail
        self.needle_trail = []

    def update(self, interval=5.0):
        self.sensor.temperature_offset = OFFSET
        self.sensor.update(interval)

        self.temperature.append(self.sensor.temperature)

        self.pressure.append(self.sensor.pressure)

        self.humidity.append(self.sensor.humidity)
        self.relative_humidity.append(self.sensor.relative_humidity)
        self.dewpoint.append(self.sensor.dewpoint)

        self.lux.append(self.sensor.lux)

        if self.sensor.updated_wind_rain:
            self.rain_total = self.sensor.rain_total
        else:
            self.rain_total = 0

        self.wind_speed.append(self.sensor.wind_speed)
        self.wind_direction.append(self.sensor.wind_direction)

        self.rain_mm_sec.append(self.sensor.rain)

        self.needle = math.radians(self.wind_direction.average(self.WIND_DIRECTION_AVERAGE_SAMPLES))
        self.needle_trail.append(self.needle)
        self.needle_trail = self.needle_trail[-self.COMPASS_TRAIL_SIZE:]


def main():
    display = ST7789.ST7789(
        rotation=90,
        port=0,
        cs=1,
        dc=9,
        backlight=12,
        spi_speed_hz=SPI_SPEED_MHZ * 1000 * 1000
    )
    image = Image.new("RGBA", (DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2), color=(255, 255, 255))
    sensordata = SensorData()
    settings = Config()
    viewcontroller = ViewController(
        (
            (
                MainView(image, sensordata, settings),
                MainViewGraph(image, sensordata, settings)
            ),
            (
                WindDirectionView(image, sensordata, settings),
                WindSpeedView(image, sensordata, settings)
            ),
            RainView(image, sensordata, settings),
            LightView(image, sensordata, settings),
            (
                TemperatureView(image, sensordata, settings),
                PressureView(image, sensordata, settings),
                HumidityView(image, sensordata, settings)
            ),
        )
    )

    while True:
        sensordata.update(interval=5.0)
        viewcontroller.update()
        viewcontroller.render()
        display.display(image.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT)).convert("RGB"))
        time.sleep(1.0 / FPS)


if __name__ == "__main__":
    main()

I’m lost in that wall of code.
@hel

Can it not be done by shoehorning the conversion formula into the OFFSET = -7.5 line?

Something like OFFSET = -7.5 * 1.8 + 32 which would convert to °F with the adjustment. Code may need refinement however. Or even at line 689. Sneaking the conversion into the def: statement there?

The actual polling of the bme280’s readings seems to be in the _init_.py could the conversion not be applied there at line 86?

I don’t have one of these boards or a spare Pi to test with. And my Python is poor at best. Just shooting the breeze.

Good luck!

1 Like

-7.5 * .1 + 36.9 this works. Thanks Larry

1 Like

That’s just adding a fixed value to degC measurements; you can’t convert to degF just by adding a constant.

Are you saying the weatherhat is
fixed on metrics.

The sensor reports in metric.

The weatherhat can display that data any way you want - in Fahrenheit or Kelvin or any other unit, but you would have to apply the appropriate conversion in weather.py to do so.

All I’m saying is, the conversion from C->F is not just a case of adding a fixed number, so just fiddling with OFFSET isn’t going to achieve what you want to achieve. Your solution is probably going to work when it’s about 50F, but get increasingly inaccurate at other temperatures.

I agree. But weather.py is a complicated program. I went through the docs and they did not provide a way to convert temperature. I would like to share my data. But right now only scientists will understand metric. Hopefully, someone from support will help. Thanks for your reply. Larry

I mean there’s quite a lot of code in there for sure, but as I said in your other thread you only really need to look at the part fetching the sensor data.

Specifically for temperature (I don’t know how you feel about the metric system for other measurements!) I would guess you want to add the conversion to line 672:

self.temperature.append(self.sensor.temperature)

to

self.temperature.append(self.sensor.temperature * 1.8 + 32)

The documents probably don’t touch on this because, frankly, Fahrenheit isn’t widely used for weather temperatures outside of one or two small countries :-)

disclaimer: I don’t own, and have never even touched a WeatherHAT (although I would like one), and I don’t often fiddle with MicroPython, but that should be … about right?

Thanks. It worked. Are you saying the United States is a small country?