Zwift Sensor Component - Feedback and Testers Needed

Awesome. Thanks @snicker

Hi Snicker, just installed the plugin from HACS and it’s working… :muscle:t2:

Is there already someone who has created a good Lovelace or Grafana setup to visualize your Zwift work-outs?

Will not speak for everybody, but my use case is limited to control fans, lighting and windows. Have some plans to create something similar to wahoo climb. So basically to momentary data, so I have removed zwift sensors from recorder to reduce diskspace usage.

Just curious… how can you remove the recording function from a sensor?

It is great project, I’ll register again zwift for this.

Through recorder option, since the sensors generate quite an amount of data in attributes

recorder:                                                                          
  exclude:
    entities:
      - sensor.zwift_cadence_xxxx
      - sensor.zwift_power_xxx
      - sensor.zwift_online_xxx

@snicker

Works perfect for me. Can control my Zwift fans. Kudos!

Thx… never knew is was this easy.

Does the refresh_interval parameter still exist? HomeAssistant tells me that it’s an invalid parameter.

Seems pretty realtime now.

FYI I’ve made these things which are nice. Not sure the best way to display yet: Get the amount of time at each power zone in the last session…

- platform: history_stats
  name: Threshold last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Threshold'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Off last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Off'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Active Recovery last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Active Recovery'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        

- platform: history_stats
  name: Endurance last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Endurance'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Tempo last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Tempo'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: VO2 Max last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'VO2 Max'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Anaerobic capacity last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Anaerobic capacity'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Neuromuscular Power last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Neuromuscular Power'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'

It runs off the power zone setup here

- platform: template
  sensors:
    zwift_ftp:
      friendly_name: "Zwift FTP"
      entity_id: sensor.zwift_online
      unit_of_measurement: 'watts'
      value_template: "{{states.sensor.zwift_online.attributes.ftp}}"
        
    zwift_power_zone:
      friendly_name: "Zwift Power Zone"   
      entity_id: sensor.zwift_power
      value_template: >-
        {% set ftp = states('sensor.zwift_ftp') | float %}
        {% set power = states('sensor.zwift_power') | float %}
        {% set zone1 = ftp | float * 0.55 %}
        {% set zone2 = ftp | float * 0.76 %}
        {% set zone3 = ftp | float * 0.90 %}
        {% set zone4 = ftp | float * 1.05 %}
        {% set zone5 = ftp | float * 1.2 %}
        {% set zone6 = ftp | float * 1.5 %}
        
        {% if power < 1 %}Off
        {% elif zone1 > power %}Active Recovery
        {% elif zone2 > power %}Endurance
        {% elif zone3 > power %}Tempo
        {% elif zone4 > power %}Threshold
        {% elif zone5 > power %}VO2 Max
        {% elif zone6 > power %}Anaerobic capacity
        {% elif zone6 < power %}Neuromuscular Power{% endif %}

and an on/off datetime like this

input_datetime:
  zwift_last_online:
    name: Zwift Last Online
    has_date: true
    has_time: true
  zwift_last_offline:
    name: Zwift Last Offline
    has_date: true
    has_time: true

These are set through node red

Seems pretty realtime now.

FYI I’ve made these things which are nice. Not sure the best way to display yet: Get the amount of time at each power zone in the last session…

- platform: history_stats
  name: Threshold last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Threshold'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Off last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Off'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Active Recovery last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Active Recovery'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        

- platform: history_stats
  name: Endurance last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Endurance'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Tempo last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Tempo'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: VO2 Max last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'VO2 Max'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Anaerobic capacity last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Anaerobic capacity'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Neuromuscular Power last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Neuromuscular Power'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'

It runs off the power zone setup here

- platform: template
  sensors:
    zwift_ftp:
      friendly_name: "Zwift FTP"
      entity_id: sensor.zwift_online
      unit_of_measurement: 'watts'
      value_template: "{{states.sensor.zwift_online.attributes.ftp}}"
        
    zwift_power_zone:
      friendly_name: "Zwift Power Zone"   
      entity_id: sensor.zwift_power
      value_template: >-
        {% set ftp = states('sensor.zwift_ftp') | float %}
        {% set power = states('sensor.zwift_power') | float %}
        {% set zone1 = ftp | float * 0.55 %}
        {% set zone2 = ftp | float * 0.76 %}
        {% set zone3 = ftp | float * 0.90 %}
        {% set zone4 = ftp | float * 1.05 %}
        {% set zone5 = ftp | float * 1.2 %}
        {% set zone6 = ftp | float * 1.5 %}
        
        {% if power < 1 %}Off
        {% elif zone1 > power %}Active Recovery
        {% elif zone2 > power %}Endurance
        {% elif zone3 > power %}Tempo
        {% elif zone4 > power %}Threshold
        {% elif zone5 > power %}VO2 Max
        {% elif zone6 > power %}Anaerobic capacity
        {% elif zone6 < power %}Neuromuscular Power{% endif %}

and an on/off datetime like this

input_datetime:
  zwift_last_online:
    name: Zwift Last Online
    has_date: true
    has_time: true
  zwift_last_offline:
    name: Zwift Last Offline
    has_date: true
    has_time: true

These are set through node red

Seems pretty realtime now.

FYI I’ve made these things which are nice. Not sure the best way to display yet: Get the amount of time at each power zone in the last session…

- platform: history_stats
  name: Threshold last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Threshold'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Off last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Off'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Active Recovery last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Active Recovery'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        

- platform: history_stats
  name: Endurance last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Endurance'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Tempo last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Tempo'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: VO2 Max last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'VO2 Max'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Anaerobic capacity last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Anaerobic capacity'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'
        
- platform: history_stats
  name: Neuromuscular Power last Zwift Session
  entity_id: sensor.zwift_power_zone
  state: 'Neuromuscular Power'
  type: time
  start: '{{ states.input_datetime.zwift_last_online.attributes.timestamp }}'  
  end: '{{ states.input_datetime.zwift_last_offline.attributes.timestamp }}'

It runs off the power zone setup here

- platform: template
  sensors:
    zwift_ftp:
      friendly_name: "Zwift FTP"
      entity_id: sensor.zwift_online
      unit_of_measurement: 'watts'
      value_template: "{{states.sensor.zwift_online.attributes.ftp}}"
        
    zwift_power_zone:
      friendly_name: "Zwift Power Zone"   
      entity_id: sensor.zwift_power
      value_template: >-
        {% set ftp = states('sensor.zwift_ftp') | float %}
        {% set power = states('sensor.zwift_power') | float %}
        {% set zone1 = ftp | float * 0.55 %}
        {% set zone2 = ftp | float * 0.76 %}
        {% set zone3 = ftp | float * 0.90 %}
        {% set zone4 = ftp | float * 1.05 %}
        {% set zone5 = ftp | float * 1.2 %}
        {% set zone6 = ftp | float * 1.5 %}
        
        {% if power < 1 %}Off
        {% elif zone1 > power %}Active Recovery
        {% elif zone2 > power %}Endurance
        {% elif zone3 > power %}Tempo
        {% elif zone4 > power %}Threshold
        {% elif zone5 > power %}VO2 Max
        {% elif zone6 > power %}Anaerobic capacity
        {% elif zone6 < power %}Neuromuscular Power{% endif %}

and an on/off datetime like this

input_datetime:
  zwift_last_online:
    name: Zwift Last Online
    has_date: true
    has_time: true
  zwift_last_offline:
    name: Zwift Last Offline
    has_date: true
    has_time: true

These are set through node red which sets them to now when distance goes above 0 and back to 0

[{"id":"52eec60f.e29148","type":"tab","label":"zwift","disabled":false,"info":""},{"id":"55730cc0.8cbe74","type":"api-call-service","z":"52eec60f.e29148","name":"","server":"2c038d05.2091b2","version":1,"service_domain":"input_datetime","service":"set_datetime","entityId":"input_datetime.zwift_last_online","data":"\t{'datetime':$now()}","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":820,"y":380,"wires":[[]]},{"id":"b617cad7.7d3738","type":"function","z":"52eec60f.e29148","name":"","func":"sold = msg.data.event.old_state.state;\nsnew = msg.data.event.new_state.state;\n\nif (sold != snew && sold === \"0.0\"){\n    msg = {\n        \"payload\": true\n    }\n    return msg;\n}\n\nelse if (snew === \"0.0\") {    \n    msg = {\n        \"payload\": false\n    }\n    return msg;\n}\n\n\nmsg = {\n    \"payload\": \"nope\"\n}\nreturn msg;","outputs":1,"noerr":0,"x":470,"y":440,"wires":[["d9a72182.4944f"]]},{"id":"a1773896.2d2e18","type":"api-current-state","z":"52eec60f.e29148","name":"","server":"2c038d05.2091b2","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"sensor.zwift_distance","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":210,"y":440,"wires":[["b617cad7.7d3738"]]},{"id":"5d4029a5.1ea498","type":"trigger-state","z":"52eec60f.e29148","name":"","server":"2c038d05.2091b2","entityid":"sensor.zwift_distance","entityidfiltertype":"exact","debugenabled":false,"constraints":[],"constraintsmustmatch":"all","outputs":2,"customoutputs":[],"outputinitially":false,"state_type":"str","x":190,"y":300,"wires":[["b617cad7.7d3738"],[]]},{"id":"d9a72182.4944f","type":"switch","z":"52eec60f.e29148","name":"","property":"payload","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":610,"y":440,"wires":[["55730cc0.8cbe74"],["4ea1644f.0a0b4c"]]},{"id":"4ea1644f.0a0b4c","type":"api-call-service","z":"52eec60f.e29148","name":"","server":"2c038d05.2091b2","version":1,"service_domain":"input_datetime","service":"set_datetime","entityId":"input_datetime.zwift_last_offline","data":"\t{'datetime':$now()}","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":820,"y":440,"wires":[[]]},{"id":"2c038d05.2091b2","type":"server","z":"","name":"Home Assistant"}]

I am so excited for it to be cold enough to Zwift again… these configurations look awesome!!

Some of us do it through the Summer :smiley:

I have a problem where it’s failing to save the _online_ entity because of the workout…

Error saving event: <Event state_changed[L]: entity_id=sensor.zwift_online_753598, ... privateAttributes=-1807501245=24953748, -1006995462=1534277136, -1741267406=15025, -51277064=6, -606378544=23389, 756053275=14, -1774820805=12, -1001004453=CYCLIST, 1017378481=606, -331985374=59, -2008990124=24965, -259609442=13, 2004261226=28796.1, -398081446=<workout_file>
  
 <description>Foundation training: Relatively low intensity ride with goal of spending a large proportion of the ride into Endurance Zone 2.</description>
  <category>6wk Beginner FTP Builder</category><WorkoutPlan>1</WorkoutPlan>
  <subcategory>Week 1</subcategory>
  <categoryIndex>3</categoryIndex>
  <name>Foundation</name>
  <authorIcon>UI/WhiteOrangeTheme/Workout_Select/brands/MarcoPinotti_Logo.tga</authorIcon>

 <categoryIndex>2</categoryIndex>

  <workout>
    <Warmup Duration="600" Zone="1"/>
    
    <SteadyState Duration="300" Zone="2"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    <SteadyState Duration="120" Zone="1"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    
    <SteadyState Duration="300" Zone="2"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    <SteadyState Duration="120" Zone="1"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    
    <SteadyState Duration="300" Zone="2"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    <SteadyState Duration="120" Zone="1"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    
    <SteadyState Duration="300" Zone="2"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    <SteadyState Duration="120" Zone="1"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    
    <SteadyState Duration="300" Zone="2"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    <SteadyState Duration="120" Zone="1"><TextEvent message="90-100 RPM Cadence!" /></SteadyState>
    
    <Cooldown Duration="300" Zone="1"/>
    
  </workout>
</workout_file>...

Any idea how/where to kill that data in Zwift or in this implementation?

I now have this lovely chart using the sensors I mentioned about and the big number card https://github.com/custom-cards/bignumber-card. Colours could do with some love!

Here is the config that makes it:

type: vertical-stack
cards:
  - type: 'custom:bignumber-card'
    entity: sensor.active_recovery_last_zwift_session
    min: 0
    max: 1
    scale: 20px
    show_state: true
    title: Active Recovery
    show_icon: false
    severity:
      - value: 0.01
        style: grey
      - value: 2
        style: grey
  - type: 'custom:bignumber-card'
    entity: sensor.endurance_last_zwift_session
    min: 0
    max: 1
    scale: 20px
    show_state: true
    title: Endurance
    show_icon: false
  - type: 'custom:bignumber-card'
    scale: 20px
    entity: sensor.tempo_last_zwift_session
    min: 0
    max: 1
    show_state: true
    title: Tempo
    show_icon: false
    severity:
      - value: 0.01
        style: 'rgb(130,212,83)'
      - value: 2
        style: 'rgb(130,212,83)'
  - type: 'custom:bignumber-card'
    entity: sensor.threshold_last_zwift_session
    scale: 20px
    min: 0
    max: 1
    show_state: true
    title: Threshold
    show_icon: false
    severity:
      - value: 0.01
        style: yellow
      - value: 2
        style: yellow
  - type: 'custom:bignumber-card'
    entity: sensor.vo2_max_last_zwift_session
    min: 0
    max: 1
    scale: 20px
    show_state: true
    title: Vo2 Max
    show_icon: false
    severity:
      - value: 0.01
        style: orange
      - value: 2
        style: orange
  - type: 'custom:bignumber-card'
    entity: sensor.anaerobic_capacity_last_zwift_session
    min: 0
    max: 1
    show_state: true
    title: Anaerobic Capacity
    show_icon: false
    scale: 20px
    severity:
      - value: 0.01
        style: red
      - value: 2
        style: red
  - type: 'custom:bignumber-card'
    entity: sensor.neuromuscular_power_last_zwift_session
    min: 0
    max: 1
    scale: 20px
    show_state: true
    title: Neuromuscular Power
    show_icon: false
    severity:
      - value: 0.01
        style: purple
      - value: 2
        style: purple

Note that this is based on a 1 hour session, you’ll need to tweak the max accordingly. It could probably use a template maybe, hmmm.

Also note the node red above seems to add UTC dates which I’ll fix and update here ASAP

refresh_interval is replaced by update_interval. This defaults to 15 seconds, but when a player is online, Zwift will be polled every second.

1 Like

@phillprice update to the latest commit in the repository and the issue with the privateAttributes should be resolved. Given that I was just dumping a ton of data into the attributes of the online sensor, it looks like a lot of unsanitary data was coming in on that privateAttributes key. It’s eliminated now

Hey .- since the update (and moving to a HACS install I have no data coming through.

Happy to debug if you have any guidance

could you share your config for the sensor? I’m at a loss since I’m running the same version and it seems to be working fine

Sure, Its not very interesting!

- platform: zwift
  username: !secret my_zwift_username
  password: !secret my_zwift_password

I’m not sure what to add add to the logger for this would it be one of these?

logger:
  default: error 
  logs:
    homeassistant.components.sensor: debug
    homeassistant.components.zwift: debug

Still not sure what could be going on. I did notice in your configurations above that your sensor names were sensor.zwift_online. However, there should be your player ID in that sensor

Yeah I removed my ID :slight_smile: