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