Wunderground Graph copycat

after tons of experimentation with different weather integrations and custom lovelace cards… I’ve finally settled on a near copycat of what i consider to be a terrific 10 day outlook, wunderground’s graph.

For a good long while, I was relying on capture-website , narrowed down to the specific DIV, and filtering out a couple headers to tighten it up… screenshot below for comparison, worked ok, but didn’t have dark mode, wasn’t interactive, and was fairly dense given the small screen realestate i afforded it on my dashboards. I set the screen capture on an hourly cron and exposed the PNG via a live camera card so it was constantly refreshed.

Also – lived in fear that the weather channel would eventually kill or significantly change the code on this page… so it was on a short list to figure out how to replicate this using raw data in HA. And I’ve done so with apexcharts and pirateweather (although you should be able to use most any weather entity with a forecast… nothing unique about my pirateweather impl):

now – i did have to first split out the forecast dict from the weather entity, a well discussed and published workaround for accessing forecast attributes:

- trigger:
    - platform: time_pattern
      minutes: 15
    - platform: homeassistant
      event: start
    - platform: event
      event_type: event_template_reloaded
  action:
    - service: weather.get_forecasts
      target:
        entity_id: weather.pirateweather
      data:
        type: hourly
      response_variable: hourly
    - service: weather.get_forecasts
      target:
        entity_id: weather.pirateweather
      data:
        type: daily
      response_variable: daily
  sensor:
    - name: pirateweather_hourly_forecast
      unique_id: pirateweather_hourly_forecast
      state: "{{ now().isoformat() }}"
      attributes:
        forecast: "{{ hourly['weather.pirateweather'].forecast[:192] }}"
    - name: pirateweather_daily_forecast
      unique_id: pirateweather_daily_forecast
      state: "{{ now().isoformat() }}"
      attributes:
        forecast: "{{ daily['weather.pirateweather'].forecast[:10] }}"

and my apex code, for anyone else that likes this approach…

type: custom:apexcharts-card
card_mod:
  style: |
    ha-card {
      border: solid 0px;
      background: none;
    }
graph_span: 7d
span:
  start: hour
update_interval: 1h
# layout: minimal
apex_config:
  # chart:
  #   height: 250px
  dataLabels:
    enabled: true
  legend:
    position: 'top'
    horizontalAlign: 'right'
    floating: true
    # fontSize: '14px'
    # show: false
  xaxis:
    labels:
      format: 'ddd'
  grid:
    show: false
  annotations:
    xaxis:
      # - x: '${new Date(states["sun.sun"].attributes.next_rising).getTime() }'
      # - x: EVAL:new Date( {{as_timestamp(states["sun.sun"].attributes.next_setting)}} )
      - x: EVAL:new Date().setHours(20,0,0)
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *1
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *1
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *1
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *2
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *2
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *2
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *3
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *3
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *3
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *4
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *4
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *4
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *5
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *5
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *5
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0)                       + 24*60*60*1000 *6
        x2: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *6
        borderColor: none
        fillColor: dimgray
        opacity: 0.2
        label:
          text: ☾
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
      - x: EVAL:new Date().setHours(20,0,0) + 10*60*60*1000      + 24*60*60*1000 *6
        borderColor: none
        label:
          text: ☼
          borderWidth: 0
          offsetX: 8
          style:
            color: gold
            background: '#0000'
    yaxis:
      - y: 10
      - y: 32
      - y: 80
# now:
#   show: true
#   color: red
yaxis:
  - id: left
    decimals: 0
    min: ~0
    max: ~80
    apex_config:
      tickAmount: 4
      forceNiceScale: true
      axisTicks:
        show: false
  - id: right
    opposite: true
    show: false
    min: 0
    max: 100
all_series_config:
  stroke_width: 2
  # extend_to_end: false
  show:
    legend_value: false
series:
  - entity: sensor.pirateweather_hourly_forecast
    name: temperature
    unit: °F
    color: red
    yaxis_id: left
    data_generator: |
      return entity.attributes.forecast.map((entry) => {
            return [new Date(entry.datetime), entry.temperature];
          });
  - entity: sensor.pirateweather_hourly_forecast
    name: feels like
    unit: °F
    color: purple
    yaxis_id: left
    data_generator: |
      return entity.attributes.forecast.map((entry) => {
            return [new Date(entry.datetime), entry.apparent_temperature];
          });
  - entity: sensor.pirateweather_hourly_forecast
    name: wind
    unit: mph
    color: green
    yaxis_id: left
    data_generator: |
      return entity.attributes.forecast.map((entry) => {
            return [new Date(entry.datetime), entry.wind_speed];
          });
  - entity: sensor.pirateweather_hourly_forecast
    name: cloud coverage
    unit: '%'
    type: area
    color: grey
    opacity: 0.5
    stroke_width: 0
    yaxis_id: right
    data_generator: |
      return entity.attributes.forecast.map((entry) => {
            return [new Date(entry.datetime), entry.cloud_coverage];
          });
  - entity: sensor.pirateweather_hourly_forecast
    name: precip
    unit: '%'
    type: area
    color: dodgerblue
    opacity: 0.6
    stroke_width: 0
    yaxis_id: right
    data_generator: |
      return entity.attributes.forecast.map((entry) => {
            var x = [new Date(entry.datetime), entry.precipitation_probability];
            return x == 0 ? null : x
          });

few notes on this apex graph:

  • it auto updates hourly, i havn’t noticed any refresh related issues after a week or so of use
  • the x-axis annotations for sunset and sunrise are hard coded. I wanted to use the sun’s next_setting and next_rising for the current day, and then some simple 24 hour math increments for the upcoming days… but i couldn’t get the template to work properly, and i found that the annotations were not auto-updated hourly. Minor inconveniences, maybe someone wants to use this or enhance it
  • i didn’t include dew point or pressure from the original inspiration, as i was trying to minimize too much on the screen, but that wouldn’t be hard to add if someone wanted
  • this is only forecast data, so the graph always starts at NOW. I set the span window to update hourly too so the period from midnight to NOW wasn’t just a blank hole… ideally i guess it’d be nice to show actual historical data there… I have not attempted to include that data in the generated data… i imagine that wouldn’t be that hard though. Might even be fun to scroll back a day or two if you wanted.
  • also have not gotten to the wind direction annotations, but no reason to think it couldn’t be added like this
3 Likes