Skip to content

Instantly share code, notes, and snippets.

@schauveau
Last active January 16, 2026 13:15
Show Gist options
  • Select an option

  • Save schauveau/30a29ec0daa50e0f1cb6d5700c068d4b to your computer and use it in GitHub Desktop.

Select an option

Save schauveau/30a29ec0daa50e0f1cb6d5700c068d4b to your computer and use it in GitHub Desktop.
Display Manual Schedule with MarstekVenusV3-modbus-TCP-IP in Home Assistant .

Display Manual Schedule with MarstekVenusV3-modbus-TCP-IP in Home Assistant

Introduction

This Gist describes how to READ and DIPLAY the Manual Schedule of Marstek Venus 3 batteries in Home Assistant using https://github.com/fonske/MarstekVenusV3-modbus-TCP-IP

It is probably applicable to other Marstek batteries with little changes.

**Note: The schedule registers are probably writable but this is not yet implemented. See the TODO list below **

Changelog:

  • 2026-01-16 : First version

Manual schedule macros for Marstek Batteries Modbus

A Venus battery provides 6 schedules each controlled using 5 consecutive Modbus 16bit registers starting at address 43100 (Venus 3)

Those 5 registers must be interpreted as follow:

  1. bitmask for the week days (bit 0=monday, 1=tuesday, ..., 6=sunday)
  2. the start time encoded as hour*100+minute
  3. the end time encoded as hour*100+minute
  4. The operating mode:
    • if 100 or more then discharging
    • if -100 or less then charging
    • if -1 then automatic (i.e. anti-feed)
    • 0 is also found in unused schedule entries.
    • Other values are unknown behavior
    • When charging and discharging, the value must not exceed the allowed limit. For the Venus3 that is typically 2500W for charging and 800W or 2500W for discharging.
  5. enable (1) or disable (0) the schedule

Also, the 5 registers are always set to 0 in unused entries.

In Home Assistant, the schedule sensor are provided by the Modbus Integration:

The macros provided in that Gist are assuming the registers are following the naming scheme used in MarstekVenusV3-modbus-TCP-IP.

For example, sensor.marstek_m3_schedule_4 contains the 4th schedule of the 3rd battery.

As of 2016-01-16, those registers are not yet declared by MarstekVenusV3-modbus-TCP-IP so you will have to add them yourself as explained below.

Installation

Step 1 - Add the required sensors in your MarstekVenusV3-modbus-TCP-IP yaml files.

Note: You can skip that step if you only care about the demo.

The attached file m1-sensor-definition.yaml contains an example for the 1st battery.

After restarting HA, new entities such as sensor.m1-sensor-definition.yaml should be provided and each one should provide a comma separated list of 5 values.

Step 2 - If it does not already exist, create a directory custom_templates in your Home Assistant configuration directory.

Step 3 - Copy the attached file marstek_schedule.jinja to the directory custom_templates.

Step 4 - Create a Markdown card with

{% import 'marstek_schedule.jinja' as ms %}
{{ ms.dump(rank=1) }}

The card should now display the schedule for the 1st battery.

(optional) A demo showing some fake schedules can be created as follow

{% import 'marstek_schedule.jinja' as ms %}
{{ ms.demo() }}

Customize the output

The dump() and demo() macros accept a printer argument to specify the macro reponsible for formatting the output.

  • to_markdown for Markdown output (default).
  • to_text for a raw text output

Example: Display the demo with the to_text printer.

{% import 'marstek_schedule.jinja' as ms %}
<pre>
{{ ms.demo(printer=ms.to_text) }}
</pre>

You can customize the output by editing to_markdown or to_text or by creating your own printer macro.

Notes for editing:

  • Use the action 'homeassistant.reload_custom_templates' after modifying this file.
  • Then force the update of the Markdown card by modifying its content.

It can be more convenient to edit a copy of the printer macro directly in a Mardown card.

In that case, the variables referenced by the copy may have to be included as well:

Example: A local copy of to_markdown in a Markdown card may requires markdown_off and markdown_on **

{% from 'marstek_schedule.jinja' import markdown_off, markdown_on %}

{%- macro my_copy_of_to_markdown(s,all=true) -%}
... # here is interesting stuff 
{%- endmacro -%}

{{ ms.demo(my_copy_of_to_markdown) }}

TODO list that I may or may not implement...

  • Create some HA script to manipulate the schedules (add, remove, enable/disable/toggle).
    • Is the firmware robust enough to handle inconsistancies without crashing?
      • overlaping schedules, illegal power values, firmware changes (e.g. anti-feed schedule since v146), ...
  • Implement a proper schedule editor in HA.
  • Indicate which schedule is currently active while in manual mode.
# REQUIRED SENSOR DEFINITIONS IN marstk_m1_modbus_tcp.yaml
#
# Warning: The definitions below must be inserted in the `sensors` list (not at the end of the file)
#
- name: "Marstek m1 Schedule 1"
unique_id: marstek_m1_schedule_1
address: 43100
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
- name: "Marstek m1 Schedule 2"
unique_id: marstek_m1_schedule_2
address: 43105
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
- name: "Marstek m1 Schedule 3"
unique_id: marstek_m1_schedule_3
address: 43110
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
- name: "Marstek m1 Schedule 4"
unique_id: marstek_m1_schedule_4
address: 43115
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
- name: "Marstek m1 Schedule 5"
unique_id: marstek_m1_schedule_5
address: 43120
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
- name: "Marstek m1 Schedule 6"
unique_id: marstek_m1_schedule_6
address: 43125
scan_interval: 90
count: 5
slave: 1
input_type: holding
data_type: custom
structure: ">HHHhH"
{# Some customizable variables #}
{%- set text_off = '\u2610' -%}
{%- set text_on = '\u2612' -%}
{%- set markdown_off = '<ha-icon icon="mdi:checkbox-blank-outline"></ha-icon>' -%}
{%- set markdown_on = '<ha-icon icon="mdi:checkbox-marked"></ha-icon>' -%}
{# decode_schedule()
Decode the comma separated list of values describing a schedule into a mapping variable
#}
{%- macro __decode_schedule(data,returns) -%}
{% set A=data.split(',') -%}
{% set bits=A[0] | int | bitwise_and(0x7F) -%}
{% set days= '{:07b}'.format(bits) | reverse -%}
{% set start_h = (A[1]|int) // 100 -%}
{% set start_m = (A[1]|int) % 100 -%}
{% set end_h = (A[2]|int) // 100 -%}
{% set end_m = (A[2]|int) % 100 -%}
{% set v = A[3]|int -%}
{% if v >= 100 -%}
{% set mode='discharge' %}
{% set arg=v %}
{% elif v <= -100 -%}
{% set mode='charge' -%}
{% set arg=-v %}
{% elif v == -1 -%}
{% set mode='auto'-%}
{% set arg=None -%}
{% elif v == 0 -%}
{% set mode='none' -%}
{% set arg=None -%}
{% else -%}
{% set mode='unknown'-%}
{% set arg=v -%}
{% endif -%}
{%- do returns( {
'days': days,
'h1': (A[1]|int)//100,
'm1': (A[1]|int)%100,
'h2': (A[2]|int)//100,
'm2': (A[2]|int)%100,
'mode': mode,
'arg': arg,
'enabled': (A[4]|int)!=0
} ) -%}
{%- endmacro -%}
{%- set decode_schedule = __decode_schedule | as_function -%}
{# to_text(s)
#
# Text printer for the schedule described by the mapping 's'.
#
# Example:
#
# {% import 'marstek_schedule.jinja' as ms %}
#
# {% set s = states('sensor.marstek_m1_schedule_3') %}
# <pre>
# {{ ms.to_text( ms.decode_schedule(s) }}
# <pre>
#
#}
{%- macro to_text(s) -%}
{%- set mode=s['mode'] -%}
{%- set h1=s['h1'] -%}
{%- set m1=s['m1'] -%}
{%- set h2=s['h2'] -%}
{%- set m2=s['m2'] -%}
{%- if mode=='charge' -%}
{%- set action='Charge %dW' | format(s['arg']) -%}
{%- elif mode=='discharge' -%}
{%- set action='Discharge %dW' | format(s['arg']) %}
{%- elif mode=='auto' %}
{%- set action='Auto ' -%}
{%- elif mode=='none' %}
{%- set action='None ' -%}
{%- elif mode=='unknown' -%}
{%- set action='Unknown(%d)'|format(s['arg']) -%}
{%- endif -%}
{%- set days = s['days'] | replace('0','-') | replace('1','x') -%}
{%- set check = s['enabled'] | iif(text_on, text_off) -%}
{{ "%s [%s] %02d:%02d → %02d:%02d %s" | format(check,days,h1,m1,h2,m2,action) }}
{%- endmacro -%}
{# to_markdown(s)
#
# Markdown printer for the schedule described by the mapping 's'.
#
# Example:
#
# {% import 'marstek_schedule.jinja' as ms %}
#
# {% set s = states('sensor.marstek_m1_schedule_3') %}
#
# {{ ms.to_markdown( ms.decode_schedule(s) }}
#
#}
{%- macro to_markdown(s) -%}
{%- set mode=s['mode'] -%}
{%- set h1=s['h1'] -%}
{%- set m1=s['m1'] -%}
{%- set h2=s['h2'] -%}
{%- set m2=s['m2'] -%}
{%- set arg=s['arg'] -%}
{%- set enabled=s['enabled'] -%}
{%- set more='' -%}
{%- if mode=='charge' -%}
{%- set iname='mdi:arrow-top-right-thick' -%}
{%- set col='#4a4' -%}
{%- set more=' %dW'|format(arg) -%}
{%- elif mode=='discharge' -%}
{%- set iname='mdi:arrow-bottom-right-thick' -%}
{%- set col='#66f' -%}
{%- set more=' %dW'|format(arg) -%}
{%- elif mode=='auto' -%}
{%- set iname='mdi:auto-mode' %}
{%- set col='#cc5' %}
{%- elif mode=='none' -%}
{%- set iname='mdi:numeric-0-circle' %}
{%- set col='gray' %}
{%- elif mode=='unknown' -%}
{%- set iname='mdi:alert-outline' -%}
{%- set col='#f55' -%}
{%- set more=' %d'|format(arg) -%}
{%- endif -%}
{%- set icon='<ha-icon icon="' ~ iname ~ '"></ha-icon>' -%}
{%- set days = s['days'] | replace('0','-') | replace('1','x') -%}
{%- if enabled -%}
{{- '<font face="monospace">%s [%s] %02d:%02d → %02d:%02d <font color="%s">%s%s</font></font>' | format(markdown_on,days,h1,m1,h2,m2,col,icon,more) -}}
{%- else -%}
{{- '<font color="grey" face="monospace">%s [%s] %02d:%02d → %02d:%02d %s%s</font>' | format(markdown_off,days,h1,m1,h2,m2,icon,more) -}}
{%- endif -%}
{%- endmacro -%}
{# dump(value, printer)
#
# Dump the schedule described by a sensor 'value' using the specified 'printer'
#
# Example:
#
# {% import 'marstek_schedule.jinja' as ms %}
#
# {{- ms.dump( states('sensor.marstek_m1_schedule_3') ) -}}
#
# <pre>
# {{- ms.dump( states('sensor.marstek_m1_schedule_3') , ms.to_text) -}}
# </pre>
#}
{% macro dump(value, printer=to_markdown) %}
{%- if value == 'unknown' -%}
unknown
{%- else -%}
{{- printer(decode_schedule(value)) }}
{%- endif -%}
{%- endmacro -%}
{# dump_all: Dump all schedules in the battery 'rank' using the specified printer
#
# Example:
#
# {% import 'marstek_schedule.jinja' as ms %}
#
# {{- ms.dump_all(1) -}}
#
# <pre>
# {{- ms.dump_all(1, ms.to_text) -}}
# </pre>
#}
{% macro dump_all(rank=1,printer=to_markdown) %}
{% set entity='sensor.marstek_m'~rank~'_schedule_' -%}
{%- for i in range(1,7) -%}
{{- dump( states(entity~i) , printer) }}
{% endfor -%}
{%- endmacro -%}
{# Some fake schedule values used by demo() #}
{% set dummy1 = "7,124,312,-750,1" %}
{% set dummy2 = "32,1230,312,1300,1" %}
{% set dummy3 = "127,124,1700,-1,1" %}
{% set dummy4 = "0,0,0,0,0" %}
{# That one contains an unknown mode (3) #}
{% set dummy5 = "64,1100,1530,3,1" %}
{% macro demo(printer=to_markdown) %}
{{ dump(dummy1,printer) }}
{{ dump(dummy2,printer) }}
{{ dump(dummy3,printer) }}
{{ dump(dummy4,printer) }}
{{ dump(dummy5,printer) }}
{%- endmacro -%}
@schauveau
Copy link
Author

Screenshot showing the demo in Markdown and text and a real schedule on a my Venus 3

The [-xx--x] in each line represents the days from Monday to Sunday.

2026-01-16T13:34:03,930318973+01:00

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment