Running Python script in HASS

Hi,

I have Home assistant running on a Rpi, (home assistant OS).
For polling my city heating i want to run a Python script, via cron.

2 Questions:
How to run a python script via cron in HASS?
Why do i get a error, i don’t get this error when running direct on a linux machine.
I ran this script via developer tools > services

Logger: homeassistant.components.python_script
Source: components/python_script/__init__.py:154
Integration: Python Scripts (documentation, issues)
First occurred: 2:16:30 PM (1 occurrences)
Last logged: 2:16:30 PM

Error loading script kamstrup.py: Line 357: "__name__" is an invalid variable name because it starts with "_"
#!/usr/bin/env python3
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42):
# <[email protected]> wrote this file.  As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
# ----------------------------------------------------------------------------
#
# Modified for Domotics and single request.
#
# Modified by Ronald van der Meer, Frank Reijn and Paul Bonnemaijers for the
# Kamstrup Multical 402
#
# Modified by Tim van Werkhoven 20201112 for generic use (e.g. mqtt/influxdb).
# Also pruned superfluous meter reading from script to save meter battery 
# life (previous version read all 30 vars and discarded unused data)
#
# Usage: __file__ <ComPort>
#

from __future__ import print_function

# You need pySerial 
import serial
import math
import sys
import datetime
import requests
import paho.mqtt.client as paho
# import urllib
# import urllib.request
import codecs

# Variables
reader = codecs.getreader("utf-8")

debug = 1

multical_var = {                # Decimal Number in Command for Kamstrup Multical
 0x003C: "Heat Energy (E1)",         #60
 0x0050: "Power",                   #80
 0x0056: "Temp1",                   #86
 0x0057: "Temp2",                   #87
 0x0059: "Tempdiff",                #89
 0x004A: "Flow",                    #74
 0x0044: "Volume",                  #68
 0x008D: "MinFlow_M",               #141
 0x008B: "MaxFlow_M",               #139
 0x008C: "MinFlowDate_M",           #140
 0x008A: "MaxFlowDate_M",           #138
 0x0091: "MinPower_M",              #145
 0x008F: "MaxPower_M",              #143
 0x0095: "AvgTemp1_M",              #149
 0x0096: "AvgTemp2_M",              #150
 0x0090: "MinPowerDate_M",          #144
 0x008E: "MaxPowerDate_M",          #142
 0x007E: "MinFlow_Y",               #126
 0x007C: "MaxFlow_Y",               #124
 0x007D: "MinFlowDate_Y",           #125
 0x007B: "MaxFlowDate_Y",           #123
 0x0082: "MinPower_Y",              #130
 0x0080: "MaxPower_Y",              #128
 0x0092: "AvgTemp1_Y",              #146
 0x0093: "AvgTemp2_Y",              #147
 0x0081: "MinPowerDate_Y",          #129
 0x007F: "MaxPowerDate_Y",          #127
 0x0061: "Temp1xm3",                #97
 0x006E: "Temp2xm3",                #110
 0x0071: "Infoevent",               #113
 0x03EC: "HourCounter",             #1004
}

multical_var_si = {                # Decimal Number in Command for Kamstrup Multical
 0x003C: 1E+0,       # source data already in Joule - "Heat Energy (E1)",         #60
 0x0050: 1E-3,       # source data in milliWatt - "Power",                   #80
 0x0056: 1E-9,       # source data in nanoCelcius - "Temp1",                   #86
 0x0057: 1E-9,       # source data in nanoCelcius - "Temp2",                   #87
 0x0059: 1E-9,       # source data in nanoCelcius - "Tempdiff",                #89
 0x004A: 1E-12/3600, # source data nanoliter/hour - "Flow",                    #74
 0x0044: 1E+0,       # source data TBD - "Volume",                  #68
 0x008D: 1E+0,       # source data TBD - "MinFlow_M",               #141
 0x008B: 1E+0,       # source data TBD - "MaxFlow_M",               #139
 0x008C: 1E+0,       # source data TBD - "MinFlowDate_M",           #140
 0x008A: 1E+0,       # source data TBD - "MaxFlowDate_M",           #138
 0x0091: 1E+0,       # source data TBD - "MinPower_M",              #145
 0x008F: 1E+0,       # source data TBD - "MaxPower_M",              #143
 0x0095: 1E+0,       # source data TBD - "AvgTemp1_M",              #149
 0x0096: 1E+0,       # source data TBD - "AvgTemp2_M",              #150
 0x0090: 1E+0,       # source data TBD - "MinPowerDate_M",          #144
 0x008E: 1E+0,       # source data TBD - "MaxPowerDate_M",          #142
 0x007E: 1E+0,       # source data TBD - "MinFlow_Y",               #126
 0x007C: 1E+0,       # source data TBD - "MaxFlow_Y",               #124
 0x007D: 1E+0,       # source data TBD - "MinFlowDate_Y",           #125
 0x007B: 1E+0,       # source data TBD - "MaxFlowDate_Y",           #123
 0x0082: 1E+0,       # source data TBD - "MinPower_Y",              #130
 0x0080: 1E+0,       # source data TBD - "MaxPower_Y",              #128
 0x0092: 1E+0,       # source data TBD - "AvgTemp1_Y",              #146
 0x0093: 1E+0,       # source data TBD - "AvgTemp2_Y",              #147
 0x0081: 1E+0,       # source data TBD - "MinPowerDate_Y",          #129
 0x007F: 1E+0,       # source data TBD - "MaxPowerDate_Y",          #127
 0x0061: 1E+0,       # source data TBD - "Temp1xm3",                #97
 0x006E: 1E+0,       # source data TBD - "Temp2xm3",                #110
 0x0071: 1E+0,       # source data TBD - "Infoevent",               #113
 0x03EC: 1E+0,       # source data TBD - "HourCounter",             #1004
}

#######################################################################
# Units, provided by Erik Jensen

units = {
    0: '', 1: 'Wh', 2: 'kWh', 3: 'MWh', 4: 'GWh', 5: 'j', 6: 'kj', 7: 'Mj',
    8: 'Gj', 9: 'Cal', 10: 'kCal', 11: 'Mcal', 12: 'Gcal', 13: 'varh',
    14: 'kvarh', 15: 'Mvarh', 16: 'Gvarh', 17: 'VAh', 18: 'kVAh',
    19: 'MVAh', 20: 'GVAh', 21: 'kW', 22: 'kW', 23: 'MW', 24: 'GW',
    25: 'kvar', 26: 'kvar', 27: 'Mvar', 28: 'Gvar', 29: 'VA', 30: 'kVA',
    31: 'MVA', 32: 'GVA', 33: 'V', 34: 'A', 35: 'kV',36: 'kA', 37: 'C',
    38: 'K', 39: 'l', 40: 'm3', 41: 'l/h', 42: 'm3/h', 43: 'm3xC',
    44: 'ton', 45: 'ton/h', 46: 'h', 47: 'hh:mm:ss', 48: 'yy:mm:dd',
    49: 'yyyy:mm:dd', 50: 'mm:dd', 51: '', 52: 'bar', 53: 'RTC',
    54: 'ASCII', 55: 'm3 x 10', 56: 'ton x 10', 57: 'GJ x 10',
    58: 'minutes', 59: 'Bitfield', 60: 's', 61: 'ms', 62: 'days',
    63: 'RTC-Q', 64: 'Datetime'
}

#######################################################################
# Kamstrup uses the "true" CCITT CRC-16
#

def crc_1021(message):
        poly = 0x1021
        reg = 0x0000
        for byte in message:
                mask = 0x80
                while(mask > 0):
                        reg<<=1
                        if byte & mask:
                                reg |= 1
                        mask>>=1
                        if reg & 0x10000:
                                reg &= 0xffff
                                reg ^= poly
        return reg

#######################################################################
# Byte values which must be escaped before transmission
#

escapes = {
    0x06: True,
    0x0d: True,
    0x1b: True,
    0x40: True,
    0x80: True,
}

#######################################################################
# And here we go....
#

class kamstrup(object):

    def __init__(self, serial_port = "/dev/ttyUSB1"):
        self.debug_fd = open("/tmp/_kamstrup", "a")
        self.debug_fd.write("\n\nStart\n")
        self.debug_id = None

        self.ser = serial.Serial(
            port = serial_port,
            baudrate = 1200,
            timeout = 5.0,
            bytesize = serial.EIGHTBITS,
            parity = serial.PARITY_NONE,
            stopbits = serial.STOPBITS_TWO)
#            xonxoff = 0,
#            rtscts = 0)
#           timeout = 20

    def debug(self, dir, b):
        for i in b:
            if dir != self.debug_id:
                if self.debug_id != None:
                    self.debug_fd.write("\n")
                self.debug_fd.write(dir + "\t")
                self.debug_id = dir
            self.debug_fd.write(" %02x " % i)
        self.debug_fd.flush()

    def debug_msg(self, msg):
        if self.debug_id != None:
            self.debug_fd.write("\n")
        self.debug_id = "Msg"
        self.debug_fd.write("Msg\t" + msg)
        self.debug_fd.flush()

    def wr(self, b):
        b = bytearray(b)
        self.debug("Wr", b);
        self.ser.write(b)

    def rd(self):
        a = self.ser.read(1)
        if len(a) == 0:
            self.debug_msg("Rx Timeout")
            return None
        b = bytearray(a)[0]
        self.debug("Rd", bytearray((b,)));
        return b

    def send(self, pfx, msg):
        b = bytearray(msg)

        b.append(0)
        b.append(0)
        c = crc_1021(b)
        b[-2] = c >> 8
        b[-1] = c & 0xff

        c = bytearray()
        c.append(pfx)
        for i in b:
            if i in escapes:
                c.append(0x1b)
                c.append(i ^ 0xff)
            else:
                c.append(i)
        c.append(0x0d)
        self.wr(c)

    def recv(self):
        b = bytearray()
        while True:
            d = self.rd()
            if d == None:
                return None
            if d == 0x40:
                b = bytearray()
            b.append(d)
            if d == 0x0d:
                break
        c = bytearray()
        i = 1;
        while i < len(b) - 1:
            if b[i] == 0x1b:
                v = b[i + 1] ^ 0xff
                if v not in escapes:
                    self.debug_msg(
                        "Missing Escape %02x" % v)
                c.append(v)
                i += 2
            else:
                c.append(b[i])
                i += 1
        if crc_1021(c):
            self.debug_msg("CRC error")
        return c[:-2]

    def readvar(self, nbr):
        # I wouldn't be surprised if you can ask for more than
        # one variable at the time, given that the length is
        # encoded in the response.  Havn't tried.

        self.send(0x80, (0x3f, 0x10, 0x01, nbr >> 8, nbr & 0xff))

        b = self.recv()
        if b == None:
            return (None, None)
        if b[0] != 0x3f or b[1] != 0x10:
            return (None, None)
        
        if b[2] != nbr >> 8 or b[3] != nbr & 0xff:
           return (None, None)

        if b[4] in units:
            u = units[b[4]]
        else:
            u = None

        # Decode the mantissa
        x = 0
        for i in range(0,b[5]):
            x <<= 8
            x |= b[i + 7]

        # Decode the exponent
        i = b[6] & 0x3f
        if b[6] & 0x40:
            i = -i
        i = math.pow(10,i)
        if b[6] & 0x80:
            i = -i
        x *= i

        if False:
            # Debug print
            s = ""
            for i in b[:4]:
                s += " %02x" % i
            s += " |"
            for i in b[4:7]:
                s += " %02x" % i
            s += " |"
            for i in b[7:]:
                s += " %02x" % i

            print(s, "=", x, units[b[4]])

        return (x, u)
            
def influxdb_update(value, prot='http', ip='127.0.0.1', port='8086', db="smarthome", querybase="energy,quantity=heat,source=multical,type=consumption value="):
    """
    Push update to influxdb with second precision
    """

    # Value is in GJ, we convert to Joule to get SI in influxdb
    value_joule = value*1000000000
    
    # Something like req_url = "http://localhost:8086/write?db=smarthometest&precision=s"
    req_url = "{}://{}:{}/write?db={}&precision=s".format(prot, ip, port, db)
    # Something like post_data = "energy,type=heat,device=landisgyr value=10"
    # Alternatively, like post_data = "energy landisgyr=10"
    post_data = "{}{:d}".format(querybase, int(value_joule))

    if debug > 0:
        print("Pushing data '{}' to influxdb".format(post_data))


    try:
        httpresponse = requests.post(req_url, data=post_data, verify=False, timeout=5)
    except Exception as inst:
        print("Could not update meter reading: {}".format(inst))
        pass

def mqtt_update(payload, ip, port, user, passwd, topic):
    """
    Publish to mqtt

    http://www.steves-internet-guide.com/publishing-messages-mqtt-client/
    https://pypi.org/project/paho-mqtt/#publishing
    """
    # broker="192.168.1.184"
    # port=1883

    client1 = paho.Client(client_id="multical")
    client1.username_pw_set(user, passwd)

    try:
        client1.connect(ip,int(port))
    except:
        print('Could not connect to mqtt broker')

    try:
        ret = client1.publish(topic, payload)
    except:
        print('Could not publish mqtt value')


if __name__ == "__main__":

    import time

#    try:
#        comport = sys.argv[1]
#    except IndexError:
#        print("Device required. Example: /dev/ttyUSB0")
#        sys.exit()
    
    # Previous Script had multiple arguments, commented this out for different usage
    #
    #command = int( sys.argv[2], 0)

    try:
        index = str( sys.argv[2] )
    except IndexError:
        print("Multical commands required.")
        sys.exit()

    index = index.split(',')

    if debug > 0:
        print("Parameter specified: ")
        for i in index:
            print("+ " + i)

    foo = kamstrup( comport )
    heat_timestamp = datetime.datetime.strftime(datetime.datetime.today(), "%Y-%m-%d %H:%M:%S" )
    

    for i in index:
        ii = int(i)
        multical_var[ii]
        x,u = foo.readvar(ii)

        # Convert to SI units
        xsi = x * multical_var_si[ii]
        
        print("{},{},{}".format(multical_var[ii], xsi, u))

    
    # influxdb_update(xsi)
    # mqtt_update(payload, ip, port, user, passwd, topic)

If by HASS you mean Home Assistant OS, you can’t, cron is not available. You can run it with an automation using one of the time triggers though.

Thanks Tom, The cron part is fixed. Now i only have to get the script itself working.

I have not used HA’s Python Script integration so I may be wrong here, but I recall looking at it a couple of years ago and found that it has to run entirely within HA’s environment so you can’t do imports and other things you would do to run it standalone.

Anyway, since you can run your python script from linux command line, you could use HA’s shell command:

and launch your python script from this. This is how I do it.

Does that work with imports or still same limitation?
I require these imports:

import sys
import socket
import binascii
import libscrc
import json

Yes you can do imports when you use the shell command

Late to this conversation. I am also trying to run some Python scripts on a time trigger. However, my scripts have some 3rd party imports. I see no way to pip install these modules in order to use them in my scripts. Am I missing something?