Skip to content

Commit

Permalink
fix: update to new HASS API and remove async-serial dependency, added…
Browse files Browse the repository at this point in the history
… installation instructions

BREAKING CHANGE: current protocol is now "standard" instead of "historical"
fixes also #1 and #2 : added installation instruction to the main README file (quick and dirty for now)
  • Loading branch information
sberthelot committed Aug 12, 2022
1 parent ea2d417 commit c504d95
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 68 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 Stéphane BERTHELOT
Copyright (c) 2021-2022 Stéphane BERTHELOT

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,106 @@
# teleinfo-home-assistant
French TIC/Teleinfo integration for Home Assistant (needs a serial dongle/interface)

>The `teleinfo-home-assistant`component is a Home Assistant custom component for getting info from a **serial** dongle
>connected to a French "TIC/Teleinfo" capable electric meter. It includes mainly old "CBE" style and new Linky appliances
Please note that the current code has been migrated from "historical" to "standard" data encoding mode. This mode
is supported only on **Linky** appliances. The default mode is "historical" and may need a change request to Enedis
or your electricity reseller to change it to "standard".

*This custom component is to be considered a hobby project, developed as I see fit, and updated when I see a need, or am inspired by a feature request. I am open for suggestions and issues detailing problems, but please don't expect me to fix it right away.*

## Installation
---
### Manual Installation
1. Copy teleinfo-home-assistant folder into your custom_components folder in your hass configuration directory.
2. Configure the `teleinfo-home-assistant` sensor (currently by editing configuration.yaml + sensor.yaml)
3. Restart Home Assistant.

### Installation with HACS (Home Assistant Community Store)
(To be tested later)

## State and attributes
---

All documented configuration items are dynamicalled reported by this integration so the list is identical to the
Enedis-Linky-NOI-CPT_54E.pdf detailed informations. Please refer to this file for explanations and available values and mesurement units.

## Configuration
---

**Please check the protocol mode of your electricity meter : "historical" or "standard", only "standard" supported for now.**

Your **configuration.yaml** file should contain :
```
sensor: !include sensor.yaml
#experimental
homeassistant:
customize: !include customize.yaml
```

Your **sensor.yaml** file would be like :
```
- platform: teleinfo
name: "Enedis teleinfo"
serial_port: '/dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_TINFO-1131-if00-port0'
# Try to use the more precise device name instead of ttyUSB0 if possible
- platform: template
sensors:
teleinfo_identifiant_compteur:
friendly_name: "Identifiant compteur"
value_template: '{{ states.sensor.enedis_teleinfo.attributes["PRM"] }}'
icon_template: mdi:eye
teleinfo_option_tarifaire:
friendly_name: "Option tarifaire"
value_template: '{{ states.sensor.enedis_teleinfo.attributes["NGTF"] }}'
teleinfo_intensite_souscrite:
friendly_name: "Intensite souscrite"
value_template: '{{ states.sensor.enedis_teleinfo.attributes["PCOUP"] | int }}'
unit_of_measurement: "A"
icon_template: mdi:current-ac
device_class: current
... add all items you need like this, don't forget to cast to int type if needed ...
```

Your **customize.yaml** file should be either **empty** or contain (__experimental__) :
```
sensor.enedis_teleinfo:
unique_id: compteur1
device_class: energy
state_class: total_increasing
sensor.teleinfo_index_base:
meter_type: '1'
meter_type_name: ELECTRIC
state_class: measurement
```
This is an experimental configuration to allow identifying the electric meter as a global power source for homeassistant
and report the global electricity consumed in the "Energy" tab and items

sensor.enedis_teleinfo:
unique_id: compteur1
device_class: energy
state_class: total_increasing
sensor.teleinfo_index_base:
meter_type: '1'
meter_type_name: ELECTRIC
state_class: measurement

## Implementation notes
---

Currently the code has been changed to synchronous IO access and opens/closes the serial port as needed.
Using asynchronous IO makes coding much more complex and would need a timer to update the status periodically, leaving
the serial port always open...
The next things to do are adding a easier configuration method (UI, ...) and allow selecting historical/standard mode

This working fine for me right now and is producing stable data over long periods, which is much better than my
previous implementation.
Some more functionnalities may be added like overconsumption alerts and so on, I will review it later (PR are welcome too ;) )
Have fun !
4 changes: 1 addition & 3 deletions custom_components/teleinfo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,5 @@
"documentation": "https://raw.githubusercontent.com/sberthelot/teleinfo-home-assistant/main/README.md",
"dependencies": [],
"codeowners": ["@sberthelot"],
"requirements": [
"pyserial-asyncio >= 0.4, <= 0.6"
]
"requirements": []
}
116 changes: 52 additions & 64 deletions custom_components/teleinfo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@
"""
import logging
import datetime
import serial

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA, STATE_CLASS_TOTAL_INCREASING, SensorEntity
from homeassistant.components.sensor import PLATFORM_SCHEMA, STATE_CLASS_TOTAL_INCREASING
from homeassistant.const import (
CONF_NAME, EVENT_HOMEASSISTANT_STOP, ATTR_ATTRIBUTION, DEVICE_CLASS_ENERGY)
from homeassistant.helpers.entity import Entity

REQUIREMENTS = ['pyserial-asyncio==0.4']

_LOGGER = logging.getLogger(__name__)

FRAME_START = '\x02'
FRAME_END = '\x03'

CONF_SERIAL_PORT = 'serial_port'

CONF_ATTRIBUTION = "Provided by EDF Teleinfo."
Expand All @@ -57,96 +60,81 @@
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})

TELEINFO_SELF_VALUE = 'EAST'

TIMEOUT = 30

async def async_setup_platform(hass, config, async_add_entities,discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Serial sensor platform."""
name = config.get(CONF_NAME)
port = config.get(CONF_SERIAL_PORT)
sensor = SerialTeleinfoSensor(name, port)

hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read())
async_add_entities([sensor], True)
add_entities([sensor], True)


class SerialTeleinfoSensor(SensorEntity):
class SerialTeleinfoSensor(Entity):
"""Representation of a Serial sensor."""

def __init__(self, name, port):
"""Initialize the Serial sensor."""
self._name = name
self._port = port
self._serial_loop_task = None
self._state = None
self._attributes = {}

async def async_added_to_hass(self):
"""Handle when an entity is about to be added to Home Assistant."""
self._serial_loop_task = self.hass.loop.create_task(
self.serial_read(self._port, baudrate=1200, bytesize=7,
parity='E', stopbits=1, rtscts=1))
self._attributes = {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
}

async def serial_read(self, device, **kwargs):
"""Process the serial data."""
import serial_asyncio
_LOGGER.debug(u"Initializing Teleinfo")
reader, _ = await serial_asyncio.open_serial_connection(url=device,
**kwargs)
def update(self):
"""Process the data."""
_LOGGER.debug("Start update")
is_over = False

is_over = True
self._reader = serial.Serial(self._port, baudrate=9600, bytesize=7,
parity='E', stopbits=1, rtscts=1, timeout=TIMEOUT)

# First read need to clear the grimlins.
line = await reader.readline()
line = self._reader.readline()
line = line.decode('ascii').replace('\r', '').replace('\n', '')

while True:
line = await reader.readline()
while FRAME_START not in line:
line = self._reader.readline()
line = line.decode('ascii').replace('\r', '').replace('\n', '')

if is_over and ('\x02' in line):
is_over = False
_LOGGER.debug(" Start Frame")
continue

if (not is_over) and ('\x03' not in line):
# Don't use strip() here because the checksum can be ' '.
if len(line.split()) == 2:
# The checksum char is ' '.
name, value = line.split()
else:
name, value = line.split()[0:2]

_LOGGER.debug(" Got : [%s] = (%s)", name, value)
self._attributes[name] = value

if name == 'BASE':
self._state = int(value)
continue

if (not is_over) and ('\x03' in line):
is_over = True
self.async_schedule_update_ha_state()
_LOGGER.debug(" End Frame")
continue

async def stop_serial_read(self):
"""Close resources."""
if self._serial_loop_task:
self._serial_loop_task.cancel()
_LOGGER.debug(" Start Frame")
line=''
while FRAME_END not in line:
line = self._reader.readline()
line = line.decode('ascii').replace('\r', '').replace('\n', '')

s = line.split('\t')
if len(s) == 3:
name = s[0]
value = s[1]
checksum = s[2]
ts = None
elif len(s) == 4:
name = s[0]
value = s[2]
checksum = s[3]
raw_ts = s[1][1:1+2*5]
ts = datetime.datetime.strptime(raw_ts, "%y%m%d%H%S")

_LOGGER.debug(" Got : [%s] = (%s)", name, value)
self._attributes[name] = value
if name == TELEINFO_SELF_VALUE:
self._state = int(self._attributes[TELEINFO_SELF_VALUE])

self._reader.close()
_LOGGER.debug(" End Frame")

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def should_poll(self):
"""No polling needed."""
return False

@property
def device_state_attributes(self):
def extra_state_attributes(self):
"""Return the state attributes."""
self._attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
return self._attributes

@property
Expand Down

0 comments on commit c504d95

Please sign in to comment.