PUN and PSV in Home Assistant
Visualizing PUN and PSV indices in Home Assistant
The following pages:
provide access to historical data for the PUN and PSV indices.
Being able to visualize these values over time directly in Home Assistant is very useful for monitoring energy costs and trends. <!– 
–>
For this reason, I created a script that fetches and exposes this data so it can be plotted inside Home Assistant.
Home Assistant configuration
In your configuration.yaml, add:
shell_command:
install_python_deps: "pip install beautifulsoup4 requests"
command_line:
- sensor:
name: PSV Gas All Data
unique_id: psv_gas_all_data
command: "python3 /config/scripts/psv_scraper.py"
# The main state becomes the 'latest' value from our JSON
value_template: "{{ value_json.latest }}"
unit_of_measurement: "€/Smc"
# This moves the 'history' array into an attribute
json_attributes:
- history
scan_interval: 31536000
- sensor:
name: PUN Luce All Data
unique_id: pun_luce_all_data
command: "python3 /config/scripts/pun_scraper.py"
value_template: "{{ value_json.latest }}"
unit_of_measurement: "€/kWh"
json_attributes:
- history
scan_interval: 31536000
template:
- sensor:
- name: "PSV Gas Prices for Chart"
unique_id: psv_gas_chart_data
state: "{{ state_attr('sensor.psv_gas_all_data', 'history') | length if state_attr('sensor.psv_gas_all_data', 'history') else 0 }}"
unit_of_measurement: "months"
attributes:
chart_data: >
{% set items = state_attr('sensor.psv_gas_all_data', 'history') %}
{% if items %}
{{ items }}
{% else %}
[]
{% endif %}
- sensor:
- name: "PUN Luce Prices for Chart"
unique_id: pun_luce_chart_data
state: "{{ state_attr('sensor.pun_luce_all_data', 'history') | length if state_attr('sensor.pun_luce_all_data', 'history') else 0 }}"
unit_of_measurement: "months"
attributes:
chart_data: >
{{ state_attr('sensor.pun_luce_all_data', 'history') or [] }}
The scripts you should place in /config/scripts/ will make the API call to get the data:
psv_scraper.py
import requests
from bs4 import BeautifulSoup
import json
def get_psv_data():
url = 'https://luceegasitalia.it/indici-pun-e-psv/psv/'
headers = {'User-Agent': 'Mozilla/5.0'}
try:
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
matrix = []
table = soup.find('table')
if table:
rows = table.find_all('tr')
for row in rows:
cells = row.find_all('td')
data = [cell.get_text(strip=True).replace('\xa0', ' ') for cell in cells]
if len(data) >= 2 and "MESE" not in data[0]:
# Convert price to float immediately for easier plotting later
try:
p_float = float(data[1].replace(',', '.'))
except:
p_float = 0
matrix.append({"month": data[0], "price": p_float})
# We return a DICT, not a list, so HA can parse it easily
return json.dumps({
"latest": matrix[0]["price"] if matrix else 0,
"history": matrix
})
except Exception as e:
return json.dumps({"error": str(e)})
if __name__ == "__main__":
print(get_psv_data())
pun_scraper.py
import requests
from bs4 import BeautifulSoup
import json
def get_pun_data():
url = 'https://luceegasitalia.it/indici-pun-e-psv/pun/'
headers = {'User-Agent': 'Mozilla/5.0'}
try:
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
matrix = []
table = soup.find('table')
if table:
rows = table.find_all('tr')
for row in rows:
cells = row.find_all('td')
data = [cell.get_text(strip=True).replace('\xa0', ' ') for cell in cells]
# MESE | F1 | F2 | F3
if len(data) >= 4 and "MESE" not in data[0]:
try:
# Convert to float immediately for HA
f1 = float(data[1].replace(',', '.'))
f2 = float(data[2].replace(',', '.'))
f3 = float(data[3].replace(',', '.'))
matrix.append({
"month": data[0],
"F1": f1,
"F2": f2,
"F3": f3
})
except ValueError:
continue
# Calculate a simple average of the most recent month for the main state
latest_avg = 0
if matrix:
latest_avg = round((matrix[0]["F1"] + matrix[0]["F2"] + matrix[0]["F3"]) / 3, 5)
return json.dumps({
"latest": latest_avg,
"history": matrix
})
except Exception as e:
return json.dumps({"error": str(e)})
if __name__ == "__main__":
print(get_pun_data())
Automations
Add the following to your automation:
alias: Update Energy Indices (PSV & PUN)
description: Forces the Python scripts to run on the 1st and 10th of the month
triggers:
- trigger: time
at: "10:00:00"
conditions:
- condition: template
value_template: ""
- condition: template
value_template: >
actions:
- action: shell_command.install_python_deps
- action: homeassistant.update_entity
target:
entity_id:
- sensor.psv_gas_all_data
- sensor.pun_luce_all_data
mode: single
In order to fill data for the first time, you can trigger the following automation manually.
This automation is required to install the required Python dependencies at boot (or alternatively install them manually using pip install beautifulsoup4 requests from the Hass.io CLI -> might break during updates), use:
alias: Install Python Deps on Boot
description: ""
triggers:
- event: start
trigger: homeassistant
actions:
- action: shell_command.install_python_deps
Custom Cards
Make sure to install ApexCharts from HACS.
Storico Prezzi PSV Gas:
type: custom:apexcharts-card
header:
show: true
title: Storico Prezzi PSV Gas
show_states: true
colorize_states: true
standard_format: false
graph_span: 72month
layout: fill
series:
- entity: sensor.psv_gas_all_data_2
attribute: history
type: line
curve: smooth
stroke_width: 3
unit: " €/Smc"
float_precision: 3
show:
datalabels: false
data_generator: |
const monthMap = {
'gennaio': 0, 'febbraio': 1, 'marzo': 2, 'aprile': 3,
'maggio': 4, 'giugno': 5, 'luglio': 6, 'agosto': 7,
'settembre': 8, 'ottobre': 9, 'novembre': 10, 'dicembre': 11
};
return entity.attributes.history.map((item) => {
const parts = item.month.toLowerCase().split(' ');
const monthNum = monthMap[parts[0]];
const year = parseInt(parts[1]);
const date = new Date(year, monthNum, 1);
const price = typeof item.price === 'string'
? parseFloat(item.price.replace(',', '.'))
: item.price;
return [date.getTime(), price];
}).sort((a, b) => a[0] - b[0]);
apex_config:
chart:
height: 400
width: 100%
toolbar:
show: false
zoom:
enabled: false
grid:
show: true
padding:
left: 10
right: 10
yaxis:
decimalsInFloat: 3
forceNiceScale: false
labels:
minWidth: 50
style:
fontSize: 12px
formatter: |
EVAL:function(value) {
return value.toFixed(3);
}
xaxis:
type: datetime
labels:
style:
fontSize: 11px
tooltip:
shared: true
"y":
formatter: |
EVAL:function(value) {
return value.toFixed(3) + ' €/Smc';
}
Storico Prezzi PUN Luce:
type: custom:apexcharts-card
header:
show: true
title: Storico Prezzi PUN Luce
show_states: true
colorize_states: true
standard_format: false
graph_span: 72month
layout: fill
series:
- entity: sensor.pun_luce_all_data
name: F1 (Peak)
type: line
curve: smooth
stroke_width: 2
unit: " €/kWh"
float_precision: 3
data_generator: |
const monthMap = {
gen: 0, gennaio: 0,
feb: 1, febbraio: 1,
mar: 2, marzo: 2,
apr: 3, aprile: 3,
mag: 4, maggio: 4,
giu: 5, giugno: 5,
lug: 6, luglio: 6,
ago: 7, agosto: 7,
set: 8, settembre: 8,
ott: 9, ottobre: 9,
nov: 10, novembre: 10,
dic: 11, dicembre: 11
};
return (entity.attributes.history || [])
.map(item => {
const [m, y] = item.month.toLowerCase().split(' ');
const monthIndex = monthMap[m];
if (monthIndex === undefined) return null;
return [new Date(parseInt(y), monthIndex, 1).getTime(), item.F1];
})
.filter(p => p !== null)
.sort((a, b) => a[0] - b[0]);
- entity: sensor.pun_luce_all_data
name: F2 (Mid)
type: line
curve: smooth
stroke_width: 2
unit: " €/kWh"
float_precision: 3
data_generator: |
const monthMap = {
gen: 0, gennaio: 0,
feb: 1, febbraio: 1,
mar: 2, marzo: 2,
apr: 3, aprile: 3,
mag: 4, maggio: 4,
giu: 5, giugno: 5,
lug: 6, luglio: 6,
ago: 7, agosto: 7,
set: 8, settembre: 8,
ott: 9, ottobre: 9,
nov: 10, novembre: 10,
dic: 11, dicembre: 11
};
return (entity.attributes.history || [])
.map(item => {
const [m, y] = item.month.toLowerCase().split(' ');
const monthIndex = monthMap[m];
if (monthIndex === undefined) return null;
return [new Date(parseInt(y), monthIndex, 1).getTime(), item.F2];
})
.filter(p => p !== null)
.sort((a, b) => a[0] - b[0]);
- entity: sensor.pun_luce_all_data
name: F3 (Off-Peak)
type: line
curve: smooth
stroke_width: 2
unit: " €/kWh"
float_precision: 3
data_generator: |
const monthMap = {
gen: 0, gennaio: 0,
feb: 1, febbraio: 1,
mar: 2, marzo: 2,
apr: 3, aprile: 3,
mag: 4, maggio: 4,
giu: 5, giugno: 5,
lug: 6, luglio: 6,
ago: 7, agosto: 7,
set: 8, settembre: 8,
ott: 9, ottobre: 9,
nov: 10, novembre: 10,
dic: 11, dicembre: 11
};
return (entity.attributes.history || [])
.map(item => {
const [m, y] = item.month.toLowerCase().split(' ');
const monthIndex = monthMap[m];
if (monthIndex === undefined) return null;
return [new Date(parseInt(y), monthIndex, 1).getTime(), item.F3];
})
.filter(p => p !== null)
.sort((a, b) => a[0] - b[0]);
apex_config:
chart:
height: 400
width: 100%
toolbar:
show: true
zoom:
enabled: true
grid:
show: true
padding:
left: 10
right: 10
xaxis:
type: datetime
labels:
style:
fontSize: 11px
yaxis:
decimalsInFloat: 3
forceNiceScale: false
labels:
formatter: |
EVAL:function(value) {
return value.toFixed(3);
}
tooltip:
shared: true
"y":
formatter: |
EVAL:function(value) {
return value.toFixed(3) + ' €/kWh';
}