mirror of https://github.com/lumapu/ahoy.git
				
				
			
							committed by
							
								 GitHub
								GitHub
							
						
					
				
				 6 changed files with 473 additions and 155 deletions
			
			
		| @ -0,0 +1,197 @@ | |||||
|  | #!/usr/bin/env python3 | ||||
|  | # -*- coding: utf-8 -*- | ||||
|  | 
 | ||||
|  | """ | ||||
|  | Hoymiles output plugin library | ||||
|  | """ | ||||
|  | 
 | ||||
|  | import socket | ||||
|  | from datetime import datetime, timezone | ||||
|  | from hoymiles.decoders import StatusResponse | ||||
|  | 
 | ||||
|  | try: | ||||
|  |     from influxdb_client import InfluxDBClient | ||||
|  | except ModuleNotFoundError: | ||||
|  |     pass | ||||
|  | 
 | ||||
|  | class OutputPluginFactory: | ||||
|  |     def __init__(self, **params): | ||||
|  |         """ | ||||
|  |         Initialize output plugin | ||||
|  | 
 | ||||
|  |         :param inverter_ser: The inverter serial | ||||
|  |         :type inverter_ser: str | ||||
|  |         :param inverter_name: The configured name for the inverter | ||||
|  |         :type inverter_name: str | ||||
|  |         """ | ||||
|  | 
 | ||||
|  |         self.inverter_ser = params.get('inverter_ser', '') | ||||
|  |         self.inverter_name = params.get('inverter_name', None) | ||||
|  | 
 | ||||
|  |     def store_status(self, response, **params): | ||||
|  |         """ | ||||
|  |         Default function | ||||
|  | 
 | ||||
|  |         :raises NotImplementedError: when the plugin does not implement store status data | ||||
|  |         """ | ||||
|  |         raise NotImplementedError('The current output plugin does not implement store_status') | ||||
|  | 
 | ||||
|  | class InfluxOutputPlugin(OutputPluginFactory): | ||||
|  |     """ Influx2 output plugin """ | ||||
|  |     api = None | ||||
|  | 
 | ||||
|  |     def __init__(self, url, token, **params): | ||||
|  |         """ | ||||
|  |         Initialize InfluxOutputPlugin | ||||
|  | 
 | ||||
|  |         The following targets must be present in your InfluxDB. This does not | ||||
|  |         automatically create anything for You. | ||||
|  | 
 | ||||
|  |         :param str url: The url to connect this client to. Like http://localhost:8086 | ||||
|  |         :param str token: Influx2 access token which is allowed to write to bucket | ||||
|  |         :param org: Influx2 org, the token belongs to | ||||
|  |         :type org: str | ||||
|  |         :param bucket: Influx2 bucket to store data in (also known as retention policy) | ||||
|  |         :type bucket: str | ||||
|  |         :param measurement: Default measurement-prefix to use | ||||
|  |         :type measurement: str | ||||
|  |         """ | ||||
|  |         super().__init__(**params) | ||||
|  | 
 | ||||
|  |         self._bucket = params.get('bucket', 'hoymiles/autogen') | ||||
|  |         self._org = params.get('org', '') | ||||
|  |         self._measurement = params.get('measurement', | ||||
|  |                 f'inverter,host={socket.gethostname()}') | ||||
|  | 
 | ||||
|  |         client = InfluxDBClient(url, token, bucket=self._bucket) | ||||
|  |         self.api = client.write_api() | ||||
|  | 
 | ||||
|  |     def store_status(self, response, **params): | ||||
|  |         """ | ||||
|  |         Publish StatusResponse object | ||||
|  | 
 | ||||
|  |         :param hoymiles.decoders.StatusResponse response: StatusResponse object | ||||
|  |         :type response: hoymiles.decoders.StatusResponse | ||||
|  |         :param measurement: Custom influx measurement name | ||||
|  |         :type measurement: str or None | ||||
|  | 
 | ||||
|  |         :raises ValueError: when response is not instance of StatusResponse | ||||
|  |         """ | ||||
|  | 
 | ||||
|  |         if not isinstance(response, StatusResponse): | ||||
|  |             raise ValueError('Data needs to be instance of StatusResponse') | ||||
|  | 
 | ||||
|  |         data = response.__dict__() | ||||
|  | 
 | ||||
|  |         measurement = self._measurement + f',location={data["inverter_ser"]}' | ||||
|  | 
 | ||||
|  |         data_stack = [] | ||||
|  | 
 | ||||
|  |         time_rx = datetime.now() | ||||
|  |         if 'time' in data and isinstance(data['time'], datetime): | ||||
|  |             time_rx = data['time'] | ||||
|  | 
 | ||||
|  |         # InfluxDB uses UTC | ||||
|  |         utctime = datetime.fromtimestamp(time_rx.timestamp(), tz=timezone.utc) | ||||
|  | 
 | ||||
|  |         # InfluxDB requires nanoseconds | ||||
|  |         ctime = int(utctime.timestamp() * 1e9) | ||||
|  | 
 | ||||
|  |         # AC Data | ||||
|  |         phase_id = 0 | ||||
|  |         for phase in data['phases']: | ||||
|  |             data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') | ||||
|  |             data_stack.append(f'{measurement},phase={phase_id},type=voltage value={phase["voltage"]} {ctime}') | ||||
|  |             data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') | ||||
|  |             phase_id = phase_id + 1 | ||||
|  | 
 | ||||
|  |         # DC Data | ||||
|  |         string_id = 0 | ||||
|  |         for string in data['strings']: | ||||
|  |             data_stack.append(f'{measurement},string={string_id},type=total value={string["energy_total"]/1000:.4f} {ctime}') | ||||
|  |             data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') | ||||
|  |             data_stack.append(f'{measurement},string={string_id},type=voltage value={string["voltage"]:.3f} {ctime}') | ||||
|  |             data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}') | ||||
|  |             string_id = string_id + 1 | ||||
|  |         # Global | ||||
|  |         data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') | ||||
|  |         data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}') | ||||
|  | 
 | ||||
|  |         self.api.write(self._bucket, self._org, data_stack) | ||||
|  | 
 | ||||
|  | try: | ||||
|  |     import paho.mqtt.client | ||||
|  | except ModuleNotFoundError: | ||||
|  |     pass | ||||
|  | 
 | ||||
|  | class MqttOutputPlugin(OutputPluginFactory): | ||||
|  |     """ Mqtt output plugin """ | ||||
|  |     client = None | ||||
|  | 
 | ||||
|  |     def __init__(self, *args, **params): | ||||
|  |         """ | ||||
|  |         Initialize MqttOutputPlugin | ||||
|  | 
 | ||||
|  |         :param host: Broker ip or hostname (defaults to: 127.0.0.1) | ||||
|  |         :type host: str | ||||
|  |         :param port: Broker port | ||||
|  |         :type port: int (defaults to: 1883) | ||||
|  |         :param user: Optional username to login to the broker | ||||
|  |         :type user: str or None | ||||
|  |         :param password: Optional passwort to login to the broker | ||||
|  |         :type password: str or None | ||||
|  |         :param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser}) | ||||
|  |         :type topic: str | ||||
|  | 
 | ||||
|  |         :param paho.mqtt.client.Client broker: mqtt-client instance | ||||
|  |         :param str inverter_ser: inverter serial | ||||
|  |         :param hoymiles.StatusResponse data: decoded inverter StatusResponse | ||||
|  |         :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) | ||||
|  |         :type topic: str | ||||
|  |         """ | ||||
|  |         super().__init__(*args, **params) | ||||
|  | 
 | ||||
|  |         mqtt_client = paho.mqtt.client.Client() | ||||
|  |         mqtt_client.username_pw_set(params.get('user', None), params.get('password', None)) | ||||
|  |         mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883)) | ||||
|  |         mqtt_client.loop_start() | ||||
|  | 
 | ||||
|  |         self.client = mqtt_client | ||||
|  | 
 | ||||
|  |     def store_status(self, response, **params): | ||||
|  |         """ | ||||
|  |         Publish StatusResponse object | ||||
|  | 
 | ||||
|  |         :param hoymiles.decoders.StatusResponse response: StatusResponse object | ||||
|  |         :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) | ||||
|  |         :type topic: str | ||||
|  | 
 | ||||
|  |         :raises ValueError: when response is not instance of StatusResponse | ||||
|  |         """ | ||||
|  | 
 | ||||
|  |         if not isinstance(response, StatusResponse): | ||||
|  |             raise ValueError('Data needs to be instance of StatusResponse') | ||||
|  | 
 | ||||
|  |         data = response.__dict__() | ||||
|  | 
 | ||||
|  |         topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') | ||||
|  | 
 | ||||
|  |         # AC Data | ||||
|  |         phase_id = 0 | ||||
|  |         for phase in data['phases']: | ||||
|  |             self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) | ||||
|  |             self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) | ||||
|  |             self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) | ||||
|  |             phase_id = phase_id + 1 | ||||
|  | 
 | ||||
|  |         # DC Data | ||||
|  |         string_id = 0 | ||||
|  |         for string in data['strings']: | ||||
|  |             self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) | ||||
|  |             self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) | ||||
|  |             self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) | ||||
|  |             self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) | ||||
|  |             string_id = string_id + 1 | ||||
|  |         # Global | ||||
|  |         self.client.publish(f'{topic}/frequency', data['frequency']) | ||||
|  |         self.client.publish(f'{topic}/temperature', data['temperature']) | ||||
| @ -0,0 +1 @@ | |||||
|  | influxdb-client>=1.28.0 | ||||
					Loading…
					
					
				
		Reference in new issue