From c717d2702dfc58bfc6ca7b3031aa70dbbcd4df77 Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Sun, 12 Nov 2023 15:58:27 -0500 Subject: [PATCH 1/3] Get JuiceBox ID and Save Config --- Dockerfile | 15 +- docker_entrypoint.sh | 206 +++++++++++++++++++++----- juicepassproxy.py | 269 +++++++++++++++++++++------------- telnet_get_juicebox_id.expect | 9 ++ telnet_get_server.expect | 4 +- 5 files changed, 355 insertions(+), 148 deletions(-) create mode 100755 telnet_get_juicebox_id.expect diff --git a/Dockerfile b/Dockerfile index e6c3349..0dd9d54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,18 +5,17 @@ ENV MQTT_HOST="127.0.0.1" ENV MQTT_PORT=1883 ENV MQTT_DISCOVERY_PREFIX="homeassistant" ENV DEVICE_NAME="JuiceBox" -ENV ENELX_PORT=8047 -ENV ENELX_SERVER="juicenet-udp-prod3-usa.enelx.com" -ENV SRC_DEFAULT="127.0.0.1" -ENV DST_DEFAULT="54.161.147.91" ENV DEBUG=false RUN pip install --upgrade pip -RUN apt-get update -RUN apt-get upgrade -y -RUN apt-get install -y git curl dnsutils net-tools telnet expect -# RUN apt-get install -y iputils-ping netcat-traditional nano # Used for debugging +RUN apt-get update && apt-get install -y gnupg curl +RUN echo "deb https://ppa.launchpadcontent.net/rmescandon/yq/ubuntu jammy main" > /etc/apt/sources.list.d/yq.list +RUN echo "deb-src https://ppa.launchpadcontent.net/rmescandon/yq/ubuntu jammy main" >> /etc/apt/sources.list.d/yq.list +RUN curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9a2d61f6bb03ced7522b8e7d6657dbe0cc86bb64' | gpg --dearmor | tee /etc/apt/trusted.gpg.d/yq.gpg +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -y git dnsutils net-tools telnet expect yq RUN git clone https://github.com/snicker/juicepassproxy.git /juicepassproxy RUN pip install --no-cache-dir -r /juicepassproxy/requirements.txt +RUN chmod +x /juicepassproxy/juicepassproxy.py /juicepassproxy/*.expect ENTRYPOINT /juicepassproxy/docker_entrypoint.sh diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh index d9a0742..e3aa257 100755 --- a/docker_entrypoint.sh +++ b/docker_entrypoint.sh @@ -1,66 +1,198 @@ #!/bin/bash -NOW=$(date +'%x %X') +CONFIG_FILE="/config/juicepassproxy.yaml" +JUICEPASSPROXY="/juicepassproxy/juicepassproxy.py" +TELNET_GET_SERVER="/juicepassproxy/telnet_get_server.expect" +TELNET_GET_JUICEBOX_ID="/juicepassproxy/telnet_get_juicebox_id.expect" + +ENELX_PORT_DEFAULT="8047" +ENELX_SERVER_DEFAULT="juicenet-udp-prod3-usa.enelx.com" +SRC_DEFAULT="127.0.0.1" +DST_DEFAULT="54.161.147.91" + +RED='\033[1;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +function logger() { + if [ "${1^^}" != "DEBUG" ] || ($DEBUG && [ "${1^^}" = "DEBUG" ]); then + if [ "${1^^}" = "ERROR" ]; then + printf "%-15s ${RED}%-10s %s${NC}\n" "$(date +'%Y-%m-%d %H:%M:%S')" "${1^^}" "${2}" + elif [ "${1^^}" = "WARNING" ]; then + printf "%-15s ${YELLOW}%-10s %s${NC}\n" "$(date +'%Y-%m-%d %H:%M:%S')" "${1^^}" "${2}" + else + printf "%-15s %-10s %s\n" "$(date +'%Y-%m-%d %H:%M:%S')" "${1^^}" "${2}" + fi + fi +} + +function valid_ip() { + local ip=$1 + local stat=1 + + if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + OIFS=$IFS + IFS='.' + ip=($ip) + IFS=$OIFS + [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ + && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] + stat=$? + fi + return $stat +} echo "--------------------------------" -echo "${NOW}: Starting JuicePassProxy" +logger info "Starting JuicePass Proxy" echo "" +logger INFO "DEBUG: ${DEBUG}" + +if test -f ${CONFIG_FILE}; then + logger DEBUG "Importing Config File" + JUICEBOX_ID_CONFIG="$(yq e '.JUICEBOX_ID' ${CONFIG_FILE})" + if [[ "${JUICEBOX_ID_CONFIG}" = "null" ]]; then + unset JUICEBOX_ID_CONFIG + fi + ENELX_SERVER_CONFIG="$(yq e '.ENELX_SERVER' ${CONFIG_FILE})" + if [[ "${ENELX_SERVER_CONFIG}" = "null" ]]; then + unset ENELX_SERVER_CONFIG + fi + ENELX_PORT_CONFIG="$(yq e '.ENELX_PORT' ${CONFIG_FILE})" + if [[ "${ENELX_PORT_CONFIG}" = "null" ]]; then + unset ENELX_PORT_CONFIG + fi + SRC_CONFIG="$(yq e '.SRC' ${CONFIG_FILE})" + if [[ "${SRC_CONFIG}" = "null" ]]; then + unset SRC_CONFIG + fi + DST_CONFIG="$(yq e '.DST' ${CONFIG_FILE})" + if [[ "${DST_CONFIG}" = "null" ]]; then + unset DST_CONFIG + fi +else + logger DEBUG "No Config File Found." +fi -if [ ! -z "${JUICEBOX_LOCAL_IP+x}" ]; then - echo "${NOW}: JUICEBOX_LOCAL_IP: ${JUICEBOX_LOCAL_IP}" - TELNET_STRING=$(/juicepassproxy/telnet_get_server.expect ${JUICEBOX_LOCAL_IP} | grep "UDPC") - retval=$? - if [ ${retval} -eq 0 ]; then - #echo "${NOW}: TELNET_STRING: ${TELNET_STRING}" - ENELX_SERVER=$(echo ${TELNET_STRING} | sed -E 's/(^.*[0-9]+.*UDPC[ ]+)(.*)(:.*)/\2/g') - #echo "${NOW}: ENELX_SERVER: ${ENELX_SERVER}" - ENELX_PORT=$(echo ${TELNET_STRING} | sed -E 's/(^.*:)(.*)([ ]+.*)/\2/g') - #echo "${NOW}: ENELX_PORT: ${ENELX_PORT}" +if [[ ! -z "${JUICEBOX_LOCAL_IP}" ]]; then + if [[ -z "${JUICEBOX_ID}" ]]; then + JUICEBOX_ID=$(${TELNET_GET_JUICEBOX_ID} ${JUICEBOX_LOCAL_IP} | sed -n 8p) + if [[ ! -z "${JUICEBOX_ID}" ]]; then + logger DEBUG "Sucessfully obtained JuiceBox ID." + JUICEBOX_ID=${JUICEBOX_ID%?} + elif [[ ! -z "${JUICEBOX_ID_CONFIG}" ]]; then + logger WARNING "Cannot get JuiceBox ID. Using config." + JUICEBOX_ID=${JUICEBOX_ID_CONFIG} + else + echo -e "\n${RED}******************************************************************************${NC}" + logger ERROR "Cannot get JuiceBox ID from Telnet. If a JuiceBox ID is later set or is obtained via Telnet, it will likely create a new JuiceBox Device with new Entities in Home Assistant." + echo -e "${RED}******************************************************************************${NC}\n" + unset JUICEBOX_ID + fi + fi + + TELNET_SERVER_STRING=$(${TELNET_GET_SERVER} ${JUICEBOX_LOCAL_IP} | grep "UDPC") + if [[ ! -z "${TELNET_SERVER_STRING}" ]]; then + logger DEBUG "Sucessfully obtained EnelX Server and Port." + #logger debug "TELNET_SERVER_STRING: ${TELNET_SERVER_STRING}" + ENELX_SERVER=$(echo ${TELNET_SERVER_STRING} | sed -E 's/(# 2 UDPC[ ]+)(.*)(:.*)/\2/g') + ENELX_PORT=$(echo ${TELNET_SERVER_STRING} | sed -E 's/(.*:)(.*)([ ]+.*)/\2/g') else - echo "ERROR getting EnelX Server from Telnet. Using defaults." + if [[ ! -z "${ENELX_SERVER_CONFIG}" ]]; then + logger WARNING "Cannot get EnelX Server from Telnet. Using config." + ENELX_SERVER=${ENELX_SERVER_CONFIG} + else + logger ERROR "Cannot get EnelX Server from Telnet. Not set in config. Using default." + ENELX_SERVER=${ENELX_SERVER_DEFAULT} + fi + if [[ ! -z "${ENELX_PORT_CONFIG}" ]]; then + logger WARNING "Cannot get EnelX Port from Telnet. Using config." + ENELX_PORT=${ENELX_PORT_CONFIG} + else + logger ERROR "Cannot get EnelX Port from Telnet. Not set in config. Using default." + ENELX_PORT=${ENELX_PORT_DEFAULT} + fi fi +else + logger DEBUG "JuiceBox Local IP not defined." fi -if [ -z "${SRC+x}" ]; then + +if [[ -z "${SRC}" ]]; then SRC=$(ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p') - retval=$? - if [ ${retval} -ne 0 ]; then - echo "ERROR getting Docker Local IP. Using default." + if valid_ip ${SRC}; then + logger DEBUG "Sucessfully obtained Docker Local IP." + elif [[ ! -z "${SRC_CONFIG}" ]]; then + logger WARNING "Cannot get Docker Local IP. Using config." + SRC=${SRC_CONFIG} + else + logger ERROR "Cannot get Docker Local IP. Not set in config. Using default." SRC=${SRC_DEFAULT} fi fi -if [ -z "${DST+x}" ]; then +if [[ -z "${DST}" ]]; then DST=$(dig +short @1.1.1.1 ${ENELX_SERVER} | awk '{ getline ; print $1 ; exit }') - retval=$? - if [ ${retval} -ne 0 ]; then - echo "ERROR getting EnelX Server IP. Using default." + if valid_ip ${DST}; then + logger DEBUG "Sucessfully obtained EnelX Server IP." + elif [[ ! -z "${DST_CONFIG}" ]]; then + logger WARNING "Cannot get EnelX Server IP. Using config." + DST=${DST_CONFIG} + else + logger ERROR "Cannot get EnelX Server IP. Not set in config. Using default." DST=${DST_DEFAULT} fi fi -JPP_STRING="python /juicepassproxy/juicepassproxy.py --src ${SRC}:${ENELX_PORT} --dst ${DST}:${ENELX_PORT} --host ${MQTT_HOST} --port ${MQTT_PORT} --discovery-prefix ${MQTT_DISCOVERY_PREFIX} --name ${DEVICE_NAME}" +JPP_STRING="python3 ${JUICEPASSPROXY} --src ${SRC}:${ENELX_PORT} --dst ${DST}:${ENELX_PORT} --host ${MQTT_HOST} --port ${MQTT_PORT} --discovery-prefix ${MQTT_DISCOVERY_PREFIX} --name ${DEVICE_NAME}" +echo "" +logger INFO "DEVICE_NAME: ${DEVICE_NAME}" +logger INFO "JUICEBOX_LOCAL_IP: ${JUICEBOX_LOCAL_IP}" +if [[ ! -z "${JUICEBOX_ID}" ]]; then + logger INFO "JUICEBOX_ID: ${JUICEBOX_ID}" + JPP_STRING+=" --juicebox-id ${JUICEBOX_ID}" +fi -echo "${NOW}: SRC: ${SRC}" -echo "${NOW}: DST: ${DST}" -echo "${NOW}: ENELX_SERVER: ${ENELX_SERVER}" -echo "${NOW}: ENELX_PORT: ${ENELX_PORT}" -echo "${NOW}: MQTT_HOST: ${MQTT_HOST}" -echo "${NOW}: MQTT_PORT: ${MQTT_PORT}" -if [ ! -z "${MQTT_USER+x}" ]; then - echo "${NOW}: MQTT_USER: ${MQTT_USER}" +logger INFO "SRC: ${SRC}" +logger INFO "DST: ${DST}" +logger INFO "ENELX_SERVER: ${ENELX_SERVER}" +logger INFO "ENELX_PORT: ${ENELX_PORT}" +logger INFO "MQTT_HOST: ${MQTT_HOST}" +logger INFO "MQTT_PORT: ${MQTT_PORT}" +if [[ ! -z "${MQTT_USER}" ]]; then + logger INFO "MQTT_USER: ${MQTT_USER}" JPP_STRING+=" --user ${MQTT_USER}" fi -if [ ! -z "${MQTT_PASS+x}" ]; then - echo "${NOW}: MQTT_PASS: $(echo ${MQTT_PASS} | sed -E 's/./*/g')" +if [[ ! -z "${MQTT_PASS}" ]]; then + logger INFO "MQTT_PASS: $(echo ${MQTT_PASS} | sed -E 's/./*/g')" JPP_STRING+=" --password ${MQTT_PASS}" fi -echo "${NOW}: MQTT_DISCOVERY_PREFIX: ${MQTT_DISCOVERY_PREFIX}" -echo "${NOW}: DEVICE_NAME: ${DEVICE_NAME}" -echo "${NOW}: DEBUG: ${DEBUG}" -if [ "${DEBUG}" = true ] ; then +logger INFO "MQTT_DISCOVERY_PREFIX: ${MQTT_DISCOVERY_PREFIX}" + +if $DEBUG; then JPP_STRING+=" --debug" fi -echo "${NOW}: COMMAND: $(echo ${JPP_STRING} | sed -E 's/(--password )(\<.*\>)(.*)/\1*****\3/')" +touch ${CONFIG_FILE} +if [[ ! -z "${JUICEBOX_ID}" ]]; then + eval "yq e -i '.JUICEBOX_ID = \"${JUICEBOX_ID}\"' ${CONFIG_FILE}" +fi +if [[ ! -z "${ENELX_SERVER}" ]]; then + eval "yq e -i '.ENELX_SERVER = \"${ENELX_SERVER}\"' ${CONFIG_FILE}" +fi +if [[ ! -z "${ENELX_PORT}" ]]; then + eval "yq e -i '.ENELX_PORT = \"${ENELX_PORT}\"' ${CONFIG_FILE}" +fi +if [[ ! -z "${SRC}" ]]; then + eval "yq e -i '.SRC = \"${SRC}\"' ${CONFIG_FILE}" +fi +if [[ ! -z "${DST}" ]]; then + eval "yq e -i '.DST = \"${DST}\"' ${CONFIG_FILE}" +fi +if $DEBUG; then + echo -e "\n${CYAN}${CONFIG_FILE}:${NC}" + yq e /config/juicepassproxy.yaml + echo "" +fi +logger INFO "COMMAND: $(echo ${JPP_STRING} | sed -E 's/(--password )(\<.*\>)(.*)/\1*****\3/')" eval ${JPP_STRING} diff --git a/juicepassproxy.py b/juicepassproxy.py index 63a0522..9ce2f06 100644 --- a/juicepassproxy.py +++ b/juicepassproxy.py @@ -1,8 +1,9 @@ -from pyproxy import pyproxy import argparse import logging -from ha_mqtt_discoverable import Settings, DeviceInfo -from ha_mqtt_discoverable.sensors import SensorInfo, Sensor + +from ha_mqtt_discoverable import DeviceInfo, Settings +from ha_mqtt_discoverable.sensors import Sensor, SensorInfo +from pyproxy import pyproxy AP_DESCRIPTION = """ Juicepass Proxy - by snicker @@ -10,7 +11,7 @@ hopefully we won't need this if EnelX fixes their API! https://github.com/home-assistant/core/issues/86588 -To get the destination IP:Port of the EnelX server, telnet to your Juicenet +To get the destination IP:Port of the EnelX server, telnet to your Juicenet device: $ telnet 192.168.x.x 2000 and give a `list` command: @@ -21,30 +22,43 @@ the address is in the UDPC line- give that an nslookup or other to determine IP juicenet-udp-prod3-usa.enelx.com - 54.161.185.130 -this may change over time- but if you are using a local DNS server to reroute -those requests to this proxy, you should stick to using the IP address here to +this may change over time- but if you are using a local DNS server to reroute +those requests to this proxy, you should stick to using the IP address here to avoid nameserver lookup loops. """ + class JuiceboxMessageHandler(object): - def __init__(self, device_name, mqtt_settings): + def __init__(self, device_name, mqtt_settings, juicebox_id=None): self.mqtt_settings = mqtt_settings self.device_name = device_name + self.juicebox_id = juicebox_id self.entities = { - 'status': None, - 'current': None, - 'frequency': None, - 'power_lifetime': None, - 'power_session': None, - 'temperature': None, - 'voltage': None + "status": None, + "current": None, + "frequency": None, + "power_lifetime": None, + "power_session": None, + "temperature": None, + "voltage": None, } self._init_devices() def _init_devices(self): - device_info = DeviceInfo(name=self.device_name, - identifiers=self.device_name, - manufacturer="EnelX") + device_info = DeviceInfo( + name=self.device_name, + identifiers=[self.juicebox_id] + if self.juicebox_id is not None + else [self.device_name], + connections=[ + ["JuiceBox ID", self.juicebox_id] + if self.juicebox_id is not None + else [] + ], + manufacturer="EnelX", + model="JuiceBox", + via_device="JuicePass Proxy", + ) self._init_device_status(device_info) self._init_device_current(device_info) self._init_device_frequency(device_info) @@ -54,78 +68,97 @@ def _init_devices(self): self._init_device_voltage(device_info) def _init_device_status(self, device_info): - name = "{} Status".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - device=device_info) + name = "Status" + sensor_info = SensorInfo( + name=name, unique_id=f"{self.juicebox_id} {name}", device=device_info + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['status'] = sensor + self.entities["status"] = sensor def _init_device_current(self, device_info): - name = "{} Current".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='measurement', - device_class="current", - unit_of_measurement='A', - device=device_info) + name = "Current" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="measurement", + device_class="current", + unit_of_measurement="A", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['current'] = sensor + self.entities["current"] = sensor def _init_device_frequency(self, device_info): - name = "{} Frequency".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='measurement', - device_class="frequency", - unit_of_measurement='Hz', - device=device_info) + name = "Frequency" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="measurement", + device_class="frequency", + unit_of_measurement="Hz", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['frequency'] = sensor + self.entities["frequency"] = sensor def _init_device_power_lifetime(self, device_info): - name = "{} Power (Lifetime)".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='total_increasing', - device_class="energy", - unit_of_measurement='Wh', - device=device_info) + name = "Power (Lifetime)" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="total_increasing", + device_class="energy", + unit_of_measurement="Wh", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['power_lifetime'] = sensor + self.entities["power_lifetime"] = sensor def _init_device_power_session(self, device_info): - name = "{} Power (Session)".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='total_increasing', - device_class="energy", - unit_of_measurement='Wh', - device=device_info) + name = "Power (Session)" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="total_increasing", + device_class="energy", + unit_of_measurement="Wh", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['power_session'] = sensor + self.entities["power_session"] = sensor def _init_device_temperature(self, device_info): - name = "{} Temperature".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='measurement', - device_class="temperature", - unit_of_measurement='°F', - device=device_info) + name = "Temperature" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="measurement", + device_class="temperature", + unit_of_measurement="°F", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['temperature'] = sensor + self.entities["temperature"] = sensor def _init_device_voltage(self, device_info): - name = "{} Voltage".format(self.device_name) - sensor_info = SensorInfo(name=name, unique_id=name, - state_class='measurement', - device_class="voltage", - unit_of_measurement='V', - device=device_info) + name = "Voltage" + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="measurement", + device_class="voltage", + unit_of_measurement="V", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['voltage'] = sensor + self.entities["voltage"] = sensor def basic_message_try_parse(self, data): message = {"type": "basic"} @@ -134,18 +167,18 @@ def basic_message_try_parse(self, data): for part in str(data).split(","): if part[0] == "S": message["status"] = { - "S0": "unplugged", - "S1": "plugged", - "S2": "charging", - "S5": "error", - "S00": "unplugged", - "S01": "plugged", - "S02": "charging", - "S05": "error" + "S0": "Unplugged", + "S1": "Plugged In", + "S2": "Charging", + "S5": "Error", + "S00": "Unplugged", + "S01": "Plugged In", + "S02": "Charging", + "S05": "Error", }.get(part) if message["status"] is None: message["status"] = "unknown {}".format(part) - active = (message["status"] == "charging") + active = message["status"].lower() == "charging" elif part[0] == "A" and active: message["current"] = round(float(part.split("A")[1]) * 0.1, 2) elif part[0] == "f": @@ -158,66 +191,100 @@ def basic_message_try_parse(self, data): message["temperature"] = round(float(part.split("T")[1]) * 1.8 + 32, 2) elif part[0] == "V": message["voltage"] = round(float(part.split("V")[1]) * 0.1, 2) + logging.debug(f"message: {message}") return message def basic_message_publish(self, message): - logging.debug('basic message {}'.format(message)) + logging.debug("basic message {}".format(message)) try: for k in message: entity = self.entities.get(k) if entity: entity.set_state(message.get(k)) - except: - logging.exception('failed to publish sensor data') + except Exception as e: + logging.exception(f"failed to publish sensor data: {e}") def remote_data_handler(self, data): - logging.debug('remote: {}'.format(data)) + logging.debug("remote: {}".format(data)) return data def local_data_handler(self, data): - logging.debug('local : {}'.format(data)) + logging.debug("local : {}".format(data)) message = self.basic_message_try_parse(data) if message: self.basic_message_publish(message) return data + def main(): - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=AP_DESCRIPTION) + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, description=AP_DESCRIPTION + ) - parser.add_argument('-s', '--src', required=True, default="127.0.0.1:8047", - help="Source IP and port, (default: %(default)s)") - parser.add_argument('-d', '--dst', required=True, - help='Destination IP and port of EnelX Server.') + parser.add_argument( + "-s", + "--src", + required=True, + default="127.0.0.1:8047", + help="Source IP and port, (default: %(default)s)", + ) + parser.add_argument( + "-d", "--dst", required=True, help="Destination IP and port of EnelX Server." + ) parser.add_argument("--debug", action="store_true") parser.add_argument("-u", "--user", type=str, help="MQTT username") parser.add_argument("-P", "--password", type=str, help="MQTT password") - parser.add_argument("-H", "--host", type=str, default="127.0.0.1", - help="MQTT hostname to connect to (default: %(default)s)") - parser.add_argument("-p", "--port", type=int, default=1883, - help="MQTT port (default: %(default)s)") - parser.add_argument("-D", "--discovery-prefix", type=str, - dest="discovery_prefix", - default="homeassistant", - help="Home Assistant MQTT topic prefix (default: %(default)s)") - parser.add_argument("--name", type=str, default="Juicebox", - help="Home Assistant Device Name (default: %(default)s)", - dest="device_name") + parser.add_argument( + "-H", + "--host", + type=str, + default="127.0.0.1", + help="MQTT hostname to connect to (default: %(default)s)", + ) + parser.add_argument( + "-p", "--port", type=int, default=1883, help="MQTT port (default: %(default)s)" + ) + parser.add_argument( + "-D", + "--discovery-prefix", + type=str, + dest="discovery_prefix", + default="homeassistant", + help="Home Assistant MQTT topic prefix (default: %(default)s)", + ) + parser.add_argument( + "--name", + type=str, + default="Juicebox", + help="Home Assistant Device Name (default: %(default)s)", + dest="device_name", + ) + parser.add_argument( + "--juicebox-id", type=str, help="JuiceBox ID", dest="juicebox_id" + ) args = parser.parse_args() if args.debug: logging.getLogger().setLevel(logging.DEBUG) - mqttsettings = Settings.MQTT(host=args.host, port=args.port, - username=args.user, password=args.password, - discovery_prefix=args.discovery_prefix) - handler = JuiceboxMessageHandler(mqtt_settings=mqttsettings, - device_name=args.device_name) + mqttsettings = Settings.MQTT( + host=args.host, + port=args.port, + username=args.user, + password=args.password, + discovery_prefix=args.discovery_prefix, + ) + handler = JuiceboxMessageHandler( + mqtt_settings=mqttsettings, + device_name=args.device_name, + juicebox_id=args.juicebox_id, + ) pyproxy.LOCAL_DATA_HANDLER = handler.local_data_handler pyproxy.REMOTE_DATA_HANDLER = handler.remote_data_handler pyproxy.udp_proxy(args.src, args.dst) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/telnet_get_juicebox_id.expect b/telnet_get_juicebox_id.expect new file mode 100755 index 0000000..11caf84 --- /dev/null +++ b/telnet_get_juicebox_id.expect @@ -0,0 +1,9 @@ +#!/usr/bin/expect + +set host [lindex $argv 0] + +set timeout 10 +spawn telnet $host 2000 +expect ">" +send "get email.name_address\r" +expect ">" diff --git a/telnet_get_server.expect b/telnet_get_server.expect index f974aca..29e4059 100755 --- a/telnet_get_server.expect +++ b/telnet_get_server.expect @@ -2,8 +2,8 @@ set host [lindex $argv 0] -set timeout 20 -spawn telnet $host 2000 +set timeout 10 +spawn telnet $host 2000 expect ">" send "list\r" expect ">" From 2eac228c2ccc4dde1eb60cc846093d39dbbbb25a Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Sun, 12 Nov 2023 16:25:02 -0500 Subject: [PATCH 2/3] Update README.MD --- README.MD | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 78f3639..e7e53a6 100644 --- a/README.MD +++ b/README.MD @@ -11,7 +11,7 @@ _Hopefully we won't need this if EnelX fixes their API!_ ## Docker Compose Installation ### Features -* If JuiceBox Local IP is defined, it will run a telnet script to get the EnelX Server and Port. +* If JuiceBox Local IP is defined, it will run a telnet script to get the EnelX Server and Port as well as the JuiceBox ID. * If DST is not defined, it will use `dig` with the CloudFlare DNS (1.1.1.1) to get the IP address of the EnelX Server and avoid a DNS lookup loop. @@ -27,6 +27,8 @@ _Hopefully we won't need this if EnelX fixes their API!_ B. Define the applicable environment variables _(see [Docker Environment Variables](#docker-environment-variables))_. + C. Specify the location for the config folder + 1. Start the Docker container. ### Example Docker Compose @@ -58,6 +60,7 @@ services: - MQTT_PASS=*** volumes: - /etc/localtime:/etc/localtime:ro + - ./config:/config ``` ### Docker Environment Variables @@ -67,6 +70,7 @@ Variable | Required | Description & Default | **JUICEBOX_LOCAL_IP** | **Recommended** | If defined, it will attempt to get the EnelX Server and Port using Telnet. If unsuccessful, it will default to the EnelX Server and Port below. **SRC** | No | If not defined, it will attempt to get the Local Docker IP. If unsuccessful, it will default to 127.0.0.1. **DST** | No | If not defined, it will attempt to get the IP of the EnelX Server. If unsuccessful, it will default to 54.161.185.130. If manually defined, you should only use the IP address of the EnelX Server and not the fully qualified domain name to avoid DNS lookup loops. +**JUICEBOX_ID** | No | If not defined, it will attempt to get the JuiceBox ID using Telnet. **ENELX_SERVER** | No | juicenet-udp-prod3-usa.enelx.com **ENELX_PORT** | No | 8047 **MQTT_HOST** | No | 127.0.0.1 From 2151caec5350a540f305d667508eb86f852aabef Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Mon, 13 Nov 2023 22:23:49 -0500 Subject: [PATCH 3/3] Formatting Cleanup --- Dockerfile | 2 +- juicepassproxy.py | 39 ++++++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0dd9d54..454078d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,6 @@ RUN apt-get update && apt-get upgrade -y RUN apt-get install -y git dnsutils net-tools telnet expect yq RUN git clone https://github.com/snicker/juicepassproxy.git /juicepassproxy RUN pip install --no-cache-dir -r /juicepassproxy/requirements.txt -RUN chmod +x /juicepassproxy/juicepassproxy.py /juicepassproxy/*.expect +RUN chmod +x /juicepassproxy/*.sh /juicepassproxy/*.expect ENTRYPOINT /juicepassproxy/docker_entrypoint.sh diff --git a/juicepassproxy.py b/juicepassproxy.py index 49f75e6..843c77c 100644 --- a/juicepassproxy.py +++ b/juicepassproxy.py @@ -34,14 +34,14 @@ def __init__(self, device_name, mqtt_settings, juicebox_id=None): self.device_name = device_name self.juicebox_id = juicebox_id self.entities = { - 'status': None, - 'current': None, - 'frequency': None, - 'energy_lifetime': None, - 'energy_session': None, - 'temperature': None, - 'voltage': None, - 'power': None + "status": None, + "current": None, + "frequency": None, + "energy_lifetime": None, + "energy_session": None, + "temperature": None, + "voltage": None, + "power": None, } self._init_devices() @@ -160,18 +160,21 @@ def _init_device_voltage(self, device_info): ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['voltage'] = sensor - + self.entities["voltage"] = sensor + def _init_device_power(self, device_info): name = "Power" - sensor_info = SensorInfo(name=name, nique_id=f"{self.juicebox_id} {name}", - state_class='measurement', - device_class="power", - unit_of_measurement='W', - device=device_info) + sensor_info = SensorInfo( + name=name, + unique_id=f"{self.juicebox_id} {name}", + state_class="measurement", + device_class="power", + unit_of_measurement="W", + device=device_info, + ) settings = Settings(mqtt=self.mqtt_settings, entity=sensor_info) sensor = Sensor(settings) - self.entities['power'] = sensor + self.entities["power"] = sensor def basic_message_try_parse(self, data): message = {"type": "basic"} @@ -204,7 +207,9 @@ def basic_message_try_parse(self, data): message["temperature"] = round(float(part.split("T")[1]) * 1.8 + 32, 2) elif part[0] == "V": message["voltage"] = round(float(part.split("V")[1]) * 0.1, 2) - message["power"] = round(message.get("voltage",0) * message.get("current",0), 2) + message["power"] = round( + message.get("voltage", 0) * message.get("current", 0), 2 + ) logging.debug(f"message: {message}") return message