Pyscript - new integration for easy and powerful Python scripting

@craigb thanks. the recent lambda change seems to help a lot.

However, I’m struggling a bit to get things rolling since it seems that I’m the jupyter kernel is quite suspectible to crashes when using lambdas.

For instance, this following (invalid) snippet causes pyscript kernel to crash. In Python 3 kernel it correctly displays the error. If I use @pyscript_compile decorated lambda, the error is also visible in pyscript kernel.

from functional import seq
seq([1,2,3]).fold_left([], lambda acc, curr: (acc.append(curr))).to_list()

Pyscript kernel (nothing in the UI but this in the terminal):

[I 13:59:59.390 NotebookApp] Saving file at /Untitled4.ipynb
hass_pyscript_kernel: iopub_port k2c: read EOF; shutdown with exit_status=1
hass_pyscript_kernel: iopub_port k2c: read EOF; shutdown with exit_status=1

Python 3 kernel:

      1 from functional import seq
----> 2 seq([1,2,3]).fold_left([], lambda acc, curr: (acc.append(curr))).to_list()

AttributeError: 'NoneType' object has no attribute 'append'

I already know my error and understand why the code fails, so it’s not that what I’m worried about. My main point is to report that certain errors, that seem to be related to lambdas, seem to crash pyscript kernel.

I tried installing functional but I get the following error in both regular python and pyscript:

ImportError: cannot import name 'seq' from 'functional'

Strangely, pip installs functional-0.4 but that latest version on PyPi is 0.7.0. It also notes that it is available in both C and python flavors (maybe the issue you have is specific to the C version - have you tried the native python version?). So I can’t replicate the problem without knowing how to install the correct version of the package.

I tried some examples that deliberately fail inside the lambda by calling a method on None, and I get the expected error. For example:

list(map(lambda x: x.count("a"), ["a", "aa", "aaa", None])) 

gives the expected error:

Exception in <jupyter_0> line 1:
    list(map(lambda x: x.count("a"), ["a", "aa", "aaa", None])) 
                                                        ^
AttributeError: 'NoneType' object has no attribute 'count'

@craigb Sorry for missing your reply. Didn’t have alerts on…

Anyhow, apologies for omitting important information. The package is called pyfunctional even though it’s imported as functional.

I’ve installed it to the host machine using requirements.txt file in pyscript folder as follows, but since Jupyter is running on my dev laptop, it’s probably not the host machine library that is used in Jupyter.

requirements.txt

pyfunctional

So, I’m running Jupyter on different host than Home Assistant where pyscript runs. Jupyter host’s manually installed version of pyfunctional is in version 1.4.3. I’ve installed it using pip3. The python interpreter I’m using is Python 3.9.1.

Ok thanks. I can replicate the error and it’s fixed with this commit:

What is the correct way of initializing pyscript.total_pris_for_strom?

Something like
if pyscript.total_pris_for_strom not in pyscript:
pyscript.total_pris_for_strom = 0

??

@time_trigger("startup")
def init_stromkostnad():
    state.persist('pyscript.total_pris_for_strom')

@time_trigger("cron(59 * * * *)")
def akkumulere_stromkostnad():
    p = round(float(sensor.total_electricity_price) * float(sensor.estimated_hourly_consumption) + float(pyscript.total_pris_for_strom), 2)
    pyscript.total_pris_for_strom = p
    sensor.total_pris_for_strom = p
    #log.info(f"Satte pris til {p}")

@time_trigger("cron(0 0 * * *)")
def nullstille_stromkostnad():
    sensor.total_pris_for_strom = 0
    pyscript.total_pris_for_strom = 0

state.persist() takes optional arguments to set a default value and/or default attributes:

state.persist(entity_id, default_value=None, default_attributes=None)

Also, it’s simpler to just call state.persist() as the script is loaded, rather than a bit later on startup:

state.persist('pyscript.total_pris_for_strom', default_value =0)

@time_trigger("cron(59 * * * *)")
def akkumulere_stromkostnad():
    ...

Edit per @stigvi: fixed keyword argument default_value

1 Like
Logger: custom_components.pyscript.file.strompris
Source: custom_components/pyscript/global_ctx.py:337
Integration: Pyscript Python scripting (documentation, issues)
First occurred: 8:55:20 (1 occurrences)
Last logged: 8:55:20

Exception in </config/pyscript/strompris.py> line 1: state.persist('pyscript.total_pris_for_strom', default=0) ^ TypeError: persist() got an unexpected keyword argument 'default'

Edit: Ups, you wrote default_value at the top and default in the example …

I see that pyscript.some_name is easily accessible in lovelace so no need to set a sensor.some_name in addition to pyscript.some_name.

The code I ended up with is shown below and is a bit shorter than the appdaemon code. Mainly because of the timer handling in appdaemon, but also because it is easier to maintain state in pyscript, it seems.

state.persist('pyscript.total_pris_for_strom', default_value=0, default_attributes={"unit_of_measurement":"NOK"})

@time_trigger("cron(59 * * * *)")
def akkumulere_stromkostnad():
    p = round(float(sensor.total_electricity_price) * float(sensor.estimated_hourly_consumption) + float(pyscript.total_pris_for_strom), 2)
    pyscript.total_pris_for_strom = p

@time_trigger("cron(0 0 * * *)")
def nullstille_stromkostnad():
    pyscript.total_pris_for_strom = 0

I am new to pyscript and currently testing it out. I can’t get the script below to trig, but the appdaemon code at the bottom works. Does someone see what is wrong with the pyscript code?

@state_trigger("sensor.passat_status.old == 'charging'")
def statusendring_ferdig():
    easee.set_charger_max_current(charger_id = "EH4", current = "6")
import appdaemon.plugins.hass.hassapi as hass

class Lading(hass.Hass):
    def initialize(self):
        self.listen_state(self.statusendring_ferdig, "sensor.passat_status", old = "charging")

    def statusendring_ferdig(self, entity, attribute, old, new, kwargs):
        self.call_service("easee/set_charger_max_current", charger_id = "EH4", current = "6")

Edit:

This works

@state_trigger("sensor.passat_status")
@state_active("sensor.passat_status.old == 'charging'")

but not this

@state_trigger("sensor.passat_status.old == 'charging'")

This is bug. Good catch! In addition to your workaround, you could mention sensor.passat_status in the trigger expression too, although it’s not intuitive:

@state_trigger("sensor.passat_status.old == 'charging' or sensor.passat_status == 'no such value'")

I just committed a fix, which you can try by using the master version:

Thanks :slight_smile:

Would this also be a valid syntax? Sensor.passat_status alone?

@state_trigger("sensor.passat_status.old == 'charging' and sensor.passat_status)

Not quite, since sensor.passat_status could be an empty string, or “0”, which will evaluate to False. It would work if you know for sure that sensor.passat_status can’t have those values. To avoid the bug you need to mention sensor.passat_status in the expression, which is why I proposed adding an or clause which would always be False.

OK. Nice to know. But I suppose @state_trigger(“sensor.passat_status”) will trig either the sensor is evaluates to true or false?

Just using a state variable (entity) name on its own has a special meaning - trigger on any change. In that case there is no expression to evaluate, so no True/False test.

One more question: Do the app configuration have to be in configuration.yaml or can I, like its done in appdaemon, have several yaml files in the pyscript directory, one yaml for each app?

I’d imagine that you could use one of the !include_dir_merge_list or !include_dir_merged_named construct in the YAML configuration to implement that. Though I don’t know if the automagic reload when changed thing would still work?

I think something like

pyscript:
 hass_is_global: true
 allow_all_imports: true
 apps: !include_dir_merge_named your_app_dir_here/

But if it’s in the configuration.yaml, I have to restart HA when making changes to an app configuration. Or have I misunderstood something?

I’d expect that you could use the “RELOAD PYSCRIPT PYTHON SCRIPTING” thing under “Configuration / Server Controls” to re-parse the YAML.

1 Like

Thanks !! Haven’t noticed that pysrcipt was there :slight_smile:
That is good enough for me, indeed.

Auto-reload detects changes to any yaml files below the pyscript directory (in addition to .py files). You could put all the configuration in pyscript/config.yaml by doing this in the main config file:

pyscript: !include pyscript/config.yaml

Within that file you could also have additional includes for each app, if you prefer to break them out.

When you change any yaml file below the pyscript directory, the entire yaml configuration will be automatically reloaded, and any config changes to each app’s config will then cause that app to be reloaded. Apps whose config is unchanged will not be reloaded.