Skip to content
This repository has been archived by the owner on Aug 29, 2024. It is now read-only.

Commit

Permalink
Added support for HomeAssistant MQTT discovery.
Browse files Browse the repository at this point in the history
smaller changes:
- added type hints
- version update
- extracted attributes of JSON payload to Enum
  • Loading branch information
ChristianKuehnel committed Jul 13, 2019
1 parent b330bd5 commit 5820238
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 32 deletions.
5 changes: 5 additions & 0 deletions plantgw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ mqtt:
#url of your mqtt server, madatory
server: my-mqtt-server

# If this is enabled, plantgateway will announce all plants via the MQTT Discovery
# feature of Home Assistant in this MQTT prefix. For details see:
# https://www.home-assistant.io/docs/mqtt/discovery/
discovery_prefix: homeassistant

#prefix of the topic where the sensor data will be published, mandatory
prefix: some/prefix/my/plants
#terminate topic with a trailing slash, optional as defaults to True
Expand Down
2 changes: 1 addition & 1 deletion plantgw/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Plantgateway version number."""

__version__ = '0.5.1'
__version__ = '0.6.0'
139 changes: 108 additions & 31 deletions plantgw/plantgw.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
##############################################


from enum import Enum
import os
import logging
import json
import time
from datetime import datetime
from typing import List, Optional
import yaml
import paho.mqtt.client as mqtt
from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_LIGHT, MI_CONDUCTIVITY, MI_MOISTURE, MI_TEMPERATURE
Expand All @@ -24,6 +26,38 @@
from plantgw import __version__


class MQTTAttributes(Enum):
"""Attributes sent in the json dict."""
BATTERY = 'battery'
TEMPERATURE = 'temperature'
BRIGHTNESS = 'brightness'
MOISTURE = 'moisture'
CONDUCTIVITY = 'conductivity'
TIMESTAMP = 'timestamp'


# unit of measurement for the different attributes
UNIT_OF_MEASUREMENT = {
MQTTAttributes.BATTERY: '%',
MQTTAttributes.TEMPERATURE: '°C',
MQTTAttributes.BRIGHTNESS: 'lux',
MQTTAttributes.MOISTURE: '%',
MQTTAttributes.CONDUCTIVITY: 'µS/cm',
MQTTAttributes.TIMESTAMP: 's',
}


# home assistant device classes for the different attributes
DEVICE_CLASS = {
MQTTAttributes.BATTERY: 'battery',
MQTTAttributes.TEMPERATURE: 'temperature',
MQTTAttributes.BRIGHTNESS: 'illuminance',
MQTTAttributes.MOISTURE: None,
MQTTAttributes.CONDUCTIVITY: None,
MQTTAttributes.TIMESTAMP: 'timestamp',
}


# pylint: disable-msg=too-many-instance-attributes
class Configuration:
"""Stores the program configuration."""
Expand All @@ -38,14 +72,15 @@ def __init__(self, config_file_path):
if 'interface' in config:
self.interface = config['interface']

self.mqtt_port = 8883
self.mqtt_user = None
self.mqtt_password = None
self.mqtt_ca_cert = None
self.mqtt_client_id = None
self.mqtt_trailing_slash = True
self.mqtt_timestamp_format = None
self.sensors = []
self.mqtt_port = 8883 # type: int
self.mqtt_user = None # type: Optional[str]
self.mqtt_password = None # type: Optional[str]
self.mqtt_ca_cert = None # type: Optional[str]
self.mqtt_client_id = None # type: Optional[str]
self.mqtt_trailing_slash = True # type:bool
self.mqtt_timestamp_format = None # type: Optional[str]
self.mqtt_discovery_prefix = None # type: Optional[str]
self.sensors = [] # type: List[SensorConfig]

if 'port' in config['mqtt']:
self.mqtt_port = config['mqtt']['port']
Expand Down Expand Up @@ -75,6 +110,9 @@ def __init__(self, config_file_path):
fail_silent = 'fail_silent' in sensor_config
self.sensors.append(SensorConfig(sensor_config['mac'], sensor_config['alias'], fail_silent))

if 'discovery_prefix' in config['mqtt']:
self.mqtt_discovery_prefix = config['mqtt']['discovery_prefix']

@staticmethod
def _configure_logging(config):
timeform = '%a, %d %b %Y %H:%M:%S'
Expand All @@ -93,7 +131,7 @@ def _configure_logging(config):
class SensorConfig:
"""Stores the configuration of a sensor."""

def __init__(self, mac, alias=None, fail_silent=False):
def __init__(self, mac: str, alias: str = None, fail_silent: bool = False):
if mac is None:
msg = 'mac of sensor must not be None'
logging.error(msg)
Expand All @@ -102,34 +140,42 @@ def __init__(self, mac, alias=None, fail_silent=False):
self.alias = alias
self.fail_silent = fail_silent

def get_topic(self):
def get_topic(self) -> str:
"""Get the topic name for the sensor."""
if self.alias is not None:
return self.alias
return self.mac

def __str__(self):
result = self.alias
def __str__(self) -> str:
if self.alias:
result = self.alias
else:
result = self.mac
if self.fail_silent:
result += ' (fail silent)'
return result

@property
def short_mac(self):
"""Get the sensor mac without ':' in it."""
return self.mac.replace(':', '')

@staticmethod
def get_name_string(sensor_list):
def get_name_string(sensor_list) -> str:
"""Convert a list of sensor objects to a nice string."""
return ', '.join([str(sensor) for sensor in sensor_list])


class PlantGateway:
"""Main class of the module."""

def __init__(self, config_file_path='~/.plantgw.yaml'):
def __init__(self, config_file_path: str = '~/.plantgw.yaml'):
config_file_path = os.path.abspath(os.path.expanduser(config_file_path))
self.config = Configuration(config_file_path)
self.config = Configuration(config_file_path) # type: Configuration
logging.info('PlantGateway version %s', __version__)
logging.info('loaded config file from %s', config_file_path)
self.mqtt_client = None
self.connected = False
self.connected = False # type: bool

def start_client(self):
"""Start the mqtt client."""
Expand Down Expand Up @@ -159,34 +205,39 @@ def _on_connect(client, _, flags, return_code):
self.mqtt_client.connect(self.config.mqtt_server, self.config.mqtt_port, 60)
self.mqtt_client.loop_start()

def _publish(self, sensor_config, poller):
def _publish(self, sensor_config: SensorConfig, poller: MiFloraPoller):
self.start_client()

prefix_fmt = '{}/{}'
if self.config.mqtt_trailing_slash:
prefix_fmt += '/'
prefix = prefix_fmt.format(self.config.mqtt_prefix, sensor_config.get_topic())
state_topic = self._get_state_topic(sensor_config)

data = {
'battery': poller.parameter_value(MI_BATTERY),
'temperature': '{0:.1f}'.format(poller.parameter_value(MI_TEMPERATURE)),
'brightness': poller.parameter_value(MI_LIGHT),
'moisture': poller.parameter_value(MI_MOISTURE),
'conductivity': poller.parameter_value(MI_CONDUCTIVITY),
'timestamp': datetime.now().isoformat(),
MQTTAttributes.BATTERY.value: poller.parameter_value(MI_BATTERY),
MQTTAttributes.TEMPERATURE.value: '{0:.1f}'.format(poller.parameter_value(MI_TEMPERATURE)),
MQTTAttributes.BRIGHTNESS.value: poller.parameter_value(MI_LIGHT),
MQTTAttributes.MOISTURE.value: poller.parameter_value(MI_MOISTURE),
MQTTAttributes.CONDUCTIVITY.value: poller.parameter_value(MI_CONDUCTIVITY),
MQTTAttributes.TIMESTAMP.value: datetime.now().isoformat(),
}
for key, value in data.items():
logging.debug("%s: %s", key, value)
if self.config.mqtt_timestamp_format is not None:
data['timestamp'] = datetime.now().strftime(self.config.mqtt_timestamp_format)
json_payload = json.dumps(data)
self.mqtt_client.publish(prefix, json_payload, qos=1, retain=True)
logging.info('sent data to topic %s', prefix)
self.mqtt_client.publish(state_topic, json_payload, qos=1, retain=True)
logging.info('sent data to topic %s', state_topic)

def _get_state_topic(self, sensor_config: SensorConfig) -> str:
prefix_fmt = '{}/{}'
if self.config.mqtt_trailing_slash:
prefix_fmt += '/'
prefix = prefix_fmt.format(self.config.mqtt_prefix,
sensor_config.get_topic())
return prefix

def process_mac(self, sensor_config):
def process_mac(self, sensor_config: SensorConfig):
"""Get data from one Sensor."""
logging.info('Getting data from sensor %s', sensor_config.get_topic())
poller = MiFloraPoller(sensor_config.mac, BluepyBackend)
self.announce_sensor(sensor_config)
self._publish(sensor_config, poller)

def process_all(self):
Expand Down Expand Up @@ -224,3 +275,29 @@ def process_all(self):

# return sensors that could not be processed after max_retry
return next_list

def announce_sensor(self, sensor_config: SensorConfig):
"""Announce the sensor via Home Assistant MQTT Discovery.
see https://www.home-assistant.io/docs/mqtt/discovery/
"""
if self.config.mqtt_discovery_prefix is None:
return
self.start_client()
device_name = f'plant_{sensor_config.short_mac}'
for attribute in MQTTAttributes:
topic = f'{self.config.mqtt_discovery_prefix}/sensor/{device_name}_{attribute.value}/config'
payload = {
'state_topic': self._get_state_topic(sensor_config),
'unit_of_measurement': UNIT_OF_MEASUREMENT[attribute],
'value_template': '{{value_json.'+attribute.value+'}}',
}
if sensor_config.alias is not None:
payload['name'] = f'{sensor_config.alias}_{attribute.value}'

if DEVICE_CLASS[attribute] is not None:
payload['device_class'] = DEVICE_CLASS[attribute]

json_payload = json.dumps(payload)
self.mqtt_client.publish(topic, json_payload, qos=1, retain=False)
logging.info('sent sensor config to topic %s', topic)

0 comments on commit 5820238

Please sign in to comment.