{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Pyscript Jupyter Tutorial\n",
    "\n",
    "## Contents\n",
    "These links should work in a live Jupyter notebook, but not the Github viewer:\n",
    "* [The Basics](#The-Basics)\n",
    "* [State Variables](#State-Variables)\n",
    "* [Triggers](#Triggers)\n",
    "    * [State Trigger](#State-Trigger)\n",
    "    * [Service Trigger](#Service-Trigger)\n",
    "    * [Time Trigger](#Time-Trigger)\n",
    "    * [Call function on startup and reload](#Call-function-on-startup-and-reload)\n",
    "    * [Event Trigger](#Event-Trigger)\n",
    "* [Task Functions](#Task-Functions)\n",
    "    * [Task Unique](#Task-Unique)\n",
    "    * [Task Wait Until](#Task-Wait-Until)\n",
    "* [Global Contexts](#Global-Contexts)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Basics\n",
    "When Juptyer notebook is connected to pyscript in HASS, it's similar to  being connected to a regular iPython interpreter.  All of pyscript's Python language features are available interactively.\n",
    "\n",
    "Assuming you have downloaded this notebook and you have [installed the pyscript Jupyter kernel](https://github.com/craigbarratt/hass-pyscript-jupyter), you can run it interactively:\n",
    "```bash\n",
    "jupyter notebook pyscript_tutorial.ipynb\n",
    "```\n",
    "\n",
    "It's assumed you know the basics of Jupyter.  To execute a cell, hit `<Shift>Enter`.  You might prefer to clear all the cell results (Cell -> All Output -> Clear) before going through the tutorial.\n",
    "\n",
    "First, we can do some simple math to make sure we are connected to the kernel:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "1 + 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To display messages use the `log.debug` (the `print` function is not currently supported)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "this text will be displayed\n"
     ]
    }
   ],
   "source": [
    "log.debug(\"this text will be displayed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Often you'll want to include variables in the displayed message, which can be done as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "this is a formatted string with variables x = 10, y = a string, list1 = [1, 2, 3]\n"
     ]
    }
   ],
   "source": [
    "x = 10\n",
    "y = \"a string\"\n",
    "list1 = [1, 2, 3]\n",
    "log.debug(f\"this is a formatted string with variables x = {x}, y = {y}, list1 = {list1}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are also related functions `log.info`, `log.warn` and `log.error` to display messages of increasing importance.  When you are running a Jupyter session, all of these log functions display their output interactively.  The messages will also appear in the HASS log file, based on the log level configuration."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## State Variables\n",
    "HASS state variables can be accessed by name.  For example, the current state of the sun is:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'above_horizon'"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sun.sun"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the HASS UI, go to Developer Tools and select that States tab to see the available state variables. State variables have attributes that can be accessed by appending the attribute name.  For example, the sun's elevation is:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "34.47"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sun.sun.elevation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Auto-completion of variable and function names is supported.  After typing the first part of the name, hit `TAB` to see the potential completions. That's a convenient way to see state variables and their attributes. Before running this next example, put your cursor at the end of the line, hit `TAB`, and select one of the options before running:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "sun.sun."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can set a state variable by assigning to it (watch the States tab in the UI to see the new state variable):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyscript.example_var = 20"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that all HASS state variables have names forced to lower-case and values forced to strings.  The value of the variable we just created is `'20'` (a str), not `20` (an int):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'20'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pyscript.example_var"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This means that statements like this will fail with an error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "'>' not supported between instances of 'str' and 'int'",
     "output_type": "error",
     "traceback": [
      "Exception in <jupyter_0> line 1:",
      "    if pyscript.example_var > 10:",
      "                              ^",
      "TypeError: '>' not supported between instances of 'str' and 'int'"
     ]
    }
   ],
   "source": [
    "if pyscript.example_var > 10:\n",
    "    log.debug(\"pyscript.example_var is greater than 10\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So instead you need to cast the value to an integer, assuming you are sure the string really does contain a valid integer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pyscript.example_var = 20, which is greater than 10\n"
     ]
    }
   ],
   "source": [
    "if int(pyscript.example_var) > 10:\n",
    "    log.debug(f\"pyscript.example_var = {pyscript.example_var}, which is greater than 10\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The functions `state.set` and `state.get` can be used to set and get state variable values, in case you want\n",
    "to compute the name dynamically.  The `state.set` function also allows you to set attributes by including them\n",
    "in a `dict`, and they maintain their Python types:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "state.set(\"pyscript.example_var\", 30, attr1=30, attr2=23.7, attr3=\"abc\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see the new attributes in the States tab, or just check them here:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "23.7"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pyscript.example_var.attr2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can define functions, use loops and `if` statements.  Let's define a function `func1` that returns two different calculations based on the sun's elevation (there's no need to cast the attribute value, since it's already `float`):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def func1(x, y):\n",
    "    if sun.sun.elevation > 30:\n",
    "        log.info(f\"The sun's elevation is {sun.sun.elevation}, which is > 30 degrees\")\n",
    "        return x + 2 * y\n",
    "    else:\n",
    "        log.info(f\"The sun's elevation is {sun.sun.elevation}, which is <= 30 degrees\")\n",
    "        return x - y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This function should be called with two arguments:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The sun's elevation is 34.47, which is > 30 degrees\n"
     ]
    }
   ],
   "source": [
    "func1(1, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Triggers\n",
    "\n",
    "### State Trigger\n",
    "The `@state_trigger` decorator specifies a Python expression that is evaluated each time the state variables it mentions change, and it calls the function when it evaluates to `True`.  Like all trigger functions, the function runs as a new asynchronous task. Here's an example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "@state_trigger(\"pyscript.state_trig_run == '1'\")\n",
    "def state_trig_func(**kwargs):\n",
    "    log.info(f\"state_trig_func called with kwargs={kwargs}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This function will be run whenever the state variable `pyscript.state_trig_run` is set to `1` (an integer) or `'1'` (a string).  We can test it by manually setting the variable, first to a value that doesn't cause the trigger:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyscript.state_trig_run = 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and next to the value that does cause it to run:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "state_trig_func called with kwargs={'trigger_type': 'state', 'var_name': 'pyscript.state_trig_run', 'value': '1', 'old_value': '2'}\n"
     ]
    }
   ],
   "source": [
    "pyscript.state_trig_run = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the function is called with several keyword parameters that capture the type of trigger, the name of the state variable that caused the trigger (useful if the expression includes several state variables) and its old and new values.\n",
    "\n",
    "Notice if you repeat that last assignment, the function doesn't get called again. HASS doesn't notify a state variable change if the value is unchanged. So to make the trigger happen again, you need to run the prior cell that sets `pyscript.state_trig_run` to a different value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can optionally specify `@state_active` and `@time_active` decorators.  These are checked whenever the trigger condition occurs (whether state, time or event), and the function will only be called if the active conditions are met.  For example, we can add these conditions to the previous example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "@state_trigger(\"pyscript.state_trig_run == '1'\")\n",
    "@state_active(\"pyscript.state_trig_active == '10'\")\n",
    "@time_active(\"range(3:00, 23:00)\")\n",
    "def state_trig_func(**kwargs):\n",
    "    log.info(f\"state_trig_func called with kwargs={kwargs}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The trigger will only happen if `pyscript.state_trig_active` is `'10'` and it's between 3am and 11pm.  First let's make sure it doesn't run with `pyscript.state_trig_active` set to the wrong value:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyscript.state_trig_active = 5\n",
    "pyscript.state_trig_run = 2\n",
    "pyscript.state_trig_run = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we'll set it to the correct value and make the trigger occur again (assuming it's between 3am and 11pm; if not adjust the time range above):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "state_trig_func called with kwargs={'trigger_type': 'state', 'var_name': 'pyscript.state_trig_run', 'value': '1', 'old_value': '2'}\n"
     ]
    }
   ],
   "source": [
    "pyscript.state_trig_active = 10\n",
    "pyscript.state_trig_run = 2\n",
    "pyscript.state_trig_run = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, let's clean up at the end of this section by deleting the function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "del state_trig_func"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Service Trigger\n",
    "The `@service` decorator causes the function to be registered as a HASS service.  The service name has a `DOMAIN` of `pyscript` and the service is the function name, so the full service name is `pyscript.<func_name>`. Whenever that service is called, the function is called with any parameters from the service call."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "@service\n",
    "def test_service(param1=None, other_param=None):\n",
    "    log.debug(f\"test_service called with param1={param1}, other_param={other_param}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The service can be called using the full service name as a function. You can also call the service from the Services tab in the Developers page of the HASS UI. These YAML arguments will specify the same parameters when calling the service\n",
    "from the HASS UI:\n",
    "```yaml\n",
    "param1:\n",
    "  45\n",
    "other_param:\n",
    "  - 6\n",
    "  - 8\n",
    "  - 12\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test_service called with param1=45, other_param=[6, 8, 12]\n"
     ]
    }
   ],
   "source": [
    "pyscript.test_service(param1=45, other_param=[6,8,12])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively, the `service.call` function can be used to call the service using the separate `DOMAIN` and service name.  You'll need to use this form if you need a expression to compute the domain or service call name, or if you happen to set DOMAIN (eg, `pyscript`) as a normal variable in your Python code, since a variable setting will override the service function call."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test_service called with param1=45, other_param=[6, 8, 12]\n"
     ]
    }
   ],
   "source": [
    "service.call(\"pyscript\", \"test_service\", param1=45, other_param=[6,8,12])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The service can be removed by undefining the Python function, or redefining it without the `@service` decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "del test_service"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should now see that the service is no longer listed in the HASS UI, and you can check using `service.has_service`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "service.has_service(\"pyscript\", \"test_service\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you are not sure what arguments are passed into a service or trigger function, define it with the `**kwargs` argument, which captures all of the keywords arguments into a `dict`.  For example, we can redefine the service function above, and when it is called you will see the `dict` displayed.  One small drawback with doing this is that the service call parameters are not know by HASS, so the UI won't prompt for them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test_service2 called with parameters {'param1': 45, 'other_param': [6, 8, 12]}\n"
     ]
    }
   ],
   "source": [
    "@service\n",
    "def test_service2(**kwargs):\n",
    "    log.debug(f\"test_service2 called with parameters {kwargs}\")\n",
    "\n",
    "pyscript.test_service2(param1=45, other_param=[6,8,12])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's clean up at the end of this section by deleting the function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "del test_service2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Time Trigger\n",
    "\n",
    "This function triggers every 5 seconds (starting at midnight every day, which is the same as continuously), but only calls the function if the condition `pyscript.ok1 == '1'` is met."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "@time_trigger(\"period(0:00, 5 sec)\")\n",
    "@state_active(\"pyscript.ok1 == '1'\")\n",
    "def every_5_seconds():\n",
    "    log.debug(\"every_5_seconds: running now\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The trigger is now occurring every 5 seconds, but the function isn't run on each trigger since the `@state_active` condition isn't `True`.  Let's set that state variable to 1, and now wait a short while to look for the output:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "every_5_seconds: running now\n",
      "every_5_seconds: running now\n",
      "every_5_seconds: running now\n"
     ]
    }
   ],
   "source": [
    "pyscript.ok1 = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now turn off the `@state_active` condition, and after waiting a while you can confirm the function is no longer being called:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyscript.ok1 = 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To disable the function, either delete it or redefine it without any triggers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "del every_5_seconds"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Call function on startup and reload\n",
    "\n",
    "With a bare `@time_trigger` the function is called on startup or load.  In Jupyter that happens whenever you\n",
    "define the function by running the cell it is defined in.  When you run this cell, the function is called\n",
    "immediately."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "on_startup: running\n",
      "on_startup: finished\n"
     ]
    }
   ],
   "source": [
    "@time_trigger\n",
    "def on_startup():\n",
    "    log.debug(\"on_startup: running\")\n",
    "    task.sleep(2)\n",
    "    log.debug(\"on_startup: finished\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Event Trigger\n",
    "Event triggers are defined with the `@event_trigger` decorator. The first argument is the event to trigger on, and the optional second argument is a Python expression based on state variables and event parameters that is evaluated when each trigger occurs, and the function is called if the condition is `True`.  Here's an example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "@event_trigger(\"my_event\")\n",
    "def got_my_event(**kwargs):\n",
    "    log.info(f\"got my_event: kwargs={kwargs}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can test this by firing an event with the `event.fire` function, or using the Events tab in the Developer page of the HASS UI."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "got my_event: kwargs={'trigger_type': 'event', 'event_type': 'my_event', 'my_param1': 100, 'fruit': 'apple'}\n"
     ]
    }
   ],
   "source": [
    "event.fire(\"my_event\", my_param1=100, fruit=\"apple\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see the function is called with the two event parameters, and also a handful of other parameters that\n",
    "tell the function the type of trigger and name of the event.\n",
    "\n",
    "Rather than use the catch-all `**kwargs`, you could explicitly specify the parameters.  In this example we'll also add a trigger condition, so that the function only runs when `my_param1` is between 50 and 150 (you could also include state variables in the condition if you want):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "@event_trigger(\"my_event\", \"50 <= my_param1 <= 150\")\n",
    "def got_my_event(my_param1=None, fruit=None):\n",
    "    log.info(f\"got my_event: my_param1={my_param1}, fruit={fruit}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now fire the event three times; you should see that only one of the events causes the function to run because of the condition we added to the event parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "got my_event: my_param1=110, fruit=orange\n"
     ]
    }
   ],
   "source": [
    "event.fire(\"my_event\", my_param1=40, fruit=\"apple\")\n",
    "event.fire(\"my_event\", my_param1=110, fruit=\"orange\")\n",
    "event.fire(\"my_event\", my_param1=180, fruit=\"banana\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As discussed in this [Wiki Page](https://github.com/custom-components/pyscript/wiki/Event-based-triggers), here's an example that listens to state change events for a particular component, in this case `sun`.  We trigger on the built-in `EVENT_STATE_CHANGED` event, and only call the function when the state variable name starts with `\"sun.\"`.  These state updates might be 4-10 minutes apart, and a lot less often at night, so you might need to wait a while to see any output:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entity sun.sun changed from above_horizon to above_horizon and attributes are now {'next_dawn': '2020-09-01T13:12:49+00:00', 'next_dusk': '2020-09-01T03:06:47+00:00', 'next_midnight': '2020-09-01T08:08:53+00:00', 'next_noon': '2020-08-31T20:15:23+00:00', 'next_rising': '2020-09-01T13:44:36+00:00', 'next_setting': '2020-09-01T02:46:15+00:00', 'elevation': 35.97, 'azimuth': 119.95, 'rising': True, 'friendly_name': 'Sun'}\n"
     ]
    }
   ],
   "source": [
    "from homeassistant.const import EVENT_STATE_CHANGED\n",
    "\n",
    "@event_trigger(EVENT_STATE_CHANGED, \"entity_id.startswith('sun.')\") \n",
    "def monitor_state_change(entity_id=None, new_state=None, old_state=None):\n",
    "    old_value = old_state.state if old_state else None\n",
    "    log.info(f\"entity {entity_id} changed from {old_value} to {new_state.state} and attributes are now {new_state.attributes}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you are tired of waiting, you could set some random state variable that starts with `\"sun.\"`, although it's not recommended to set state variable in another component's address space unless you are sure it won't conflict with a name that component might use:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entity sun.my_new_var changed from None to 13 and attributes are now {}\n"
     ]
    }
   ],
   "source": [
    "sun.my_new_var = 13"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's much better to use `@state_trigger` decorators to trigger on state variable changes, rather than triggering on all `EVENT_STATE_CHANGED` events as we did in this example.\n",
    "\n",
    "Finally, let's delete this event trigger before moving on:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "del monitor_state_change"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Task Functions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Task Unique\n",
    "There are several features that allow functions to sleep, wait for additional triggers or kill other tasks. Pyscript allows functions that are triggered to be long-running since they run as separate asynchronous tasks.  Let's create a service to demonstrate some of these features:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [],
   "source": [
    "@service\n",
    "def long_running(name=\"task\", loop_cnt=10):\n",
    "    log.debug(f\"{name}: starting\")\n",
    "    for i in range(loop_cnt):\n",
    "        task.sleep(3)\n",
    "        log.debug(f\"{name}:     completed loop #{i}\")\n",
    "    log.debug(f\"{name}: finished\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This function runs a certain number of loops, each of which sleeps for 3 seconds and displays a message.  Let's test it by calling the service:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "task: starting\n",
      "task:     completed loop #0\n",
      "task:     completed loop #1\n",
      "task:     completed loop #2\n",
      "task: finished\n"
     ]
    }
   ],
   "source": [
    "pyscript.long_running(loop_cnt=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The service triggers the function, which runs as a new asynchronous task.  Let's do two service calls, using the optional `name` parameter so we can distinguish the output:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "task0: starting\n",
      "task1: starting\n",
      "task0:     completed loop #0\n",
      "task1:     completed loop #0\n",
      "task0:     completed loop #1\n",
      "task1:     completed loop #1\n",
      "task0:     completed loop #2\n",
      "task0: finished\n",
      "task1:     completed loop #2\n",
      "task1:     completed loop #3\n",
      "task1: finished\n"
     ]
    }
   ],
   "source": [
    "pyscript.long_running(name=\"task0\", loop_cnt=3)\n",
    "pyscript.long_running(name=\"task1\", loop_cnt=4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that each function is running independently, starting at almost the same time.  The second service call runs one more loop so it finishes about 3 seconds after the first.\n",
    "\n",
    "Depending on our application, we might prefer that there is only one such long-running function.  We can accomplish this with `task.unique`. When `task.unique` is called with a string argument, any task that is currently running that called `task.unique` with that same argument will be killed. Let's add a call to `task.unique` to our function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [],
   "source": [
    "@service\n",
    "def long_running(name=\"task\", loop_cnt=10):\n",
    "    task.unique(\"long_running_service\")\n",
    "    log.debug(f\"{name}: starting\")\n",
    "    for i in range(loop_cnt):\n",
    "        task.sleep(3)\n",
    "        log.debug(f\"{name}:     completed loop #{i}\")\n",
    "    log.debug(f\"{name}: finished\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's start a longer service run:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "task: starting\n",
      "task:     completed loop #0\n",
      "task:     completed loop #1\n",
      "task:     completed loop #2\n"
     ]
    }
   ],
   "source": [
    "pyscript.long_running(loop_cnt=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and after you've seen a couple of lines of output, let's call `task.unique` with the same argument:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "task.unique(\"long_running_service\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Normally you wouldn't call `task.unique` outside a function, but it is a good way to stop an existing long-running function, since redefining it won't stop the current running instance.  Now let's make two service calls like before, and see what happens. We wait a short while before the second service call:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "task0: starting\n",
      "task1: starting\n",
      "task1:     completed loop #0\n",
      "task1:     completed loop #1\n",
      "task1:     completed loop #2\n",
      "task1:     completed loop #3\n",
      "task1: finished\n"
     ]
    }
   ],
   "source": [
    "pyscript.long_running(name=\"task0\", loop_cnt=3)\n",
    "task.sleep(0.5)\n",
    "pyscript.long_running(name=\"task1\", loop_cnt=4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that `task0` does start, but once `task1` starts, its call to `task.unique` causes `task0` to be killed.\n",
    "\n",
    "`task.unique` takes an argument that reverses the behavior - the new task is killed and the old one keeps running:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "@service\n",
    "def long_running(name=\"task\", loop_cnt=10):\n",
    "    task.unique(\"long_running_service\", kill_me=True)\n",
    "    log.debug(f\"{name}: starting\")\n",
    "    for i in range(loop_cnt):\n",
    "        task.sleep(3)\n",
    "        log.debug(f\"{name}:     completed loop #{i}\")\n",
    "    log.debug(f\"{name}: finished\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we'll call the same service twice:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "task0: starting\n",
      "task0:     completed loop #0\n",
      "task0:     completed loop #1\n",
      "task0:     completed loop #2\n",
      "task0: finished\n"
     ]
    }
   ],
   "source": [
    "pyscript.long_running(name=\"task0\", loop_cnt=3)\n",
    "task.sleep(0.5)\n",
    "pyscript.long_running(name=\"task1\", loop_cnt=4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As expected, the first service call continues and the second service call is terminated as soon as it calls `task.unique`.\n",
    "\n",
    "Now let's clean up the function before moving on."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [],
   "source": [
    "del long_running"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Task Wait Until\n",
    "The function `task.wait_until` can be used to wait for additional state, time or event triggers. Three keyword arguments, `state_trigger`, `time_trigger` and `event_trigger` match the corresponding trigger decorators. There is an overall optional `timeout` in seconds, which causes `task.wait_until` to return when the timeout expires if no triggers have occurred. See the [README](https://github.com/custom-components/pyscript#waiting-for-events) for more information.\n",
    "\n",
    "In this example, we have two binary sensors and we want to know if sensor2 is active within 10 seconds of sensor1 becoming active:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "@state_trigger(\"input_boolean.test_sensor1 == 'on'\")\n",
    "def sensor1_active():\n",
    "    task.unique('sensor1_active')\n",
    "    log.debug(\"waiting for sensor2\")\n",
    "    result = task.wait_until(state_trigger=\"input_boolean.test_sensor2 == 'on'\", timeout=10)\n",
    "    log.debug(f\"result = {result}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's turn both sensors off, turn on sensor1, wait for less than 10 seconds, then turn sensor2 on:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "waiting for sensor2\n",
      "result = {'trigger_type': 'state', 'var_name': 'input_boolean.test_sensor2', 'value': 'on', 'old_value': 'off'}\n"
     ]
    }
   ],
   "source": [
    "# reset both sensors\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"off\"\n",
    "# this will trigger the function\n",
    "input_boolean.test_sensor1 = \"on\"\n",
    "# wait a few seconds, then turn sensor2 on:\n",
    "task.sleep(3)\n",
    "input_boolean.test_sensor2 = \"on\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that `task.wait_until` returns as soon as sensor2 is set to `on`. The return `dict` shows what caused the function to return (in this case a state trigger), the name of the variable that caused the trigger, and its new and old values. If we re-run this cell with a 12 second delay, now the timeout will cause the return:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "waiting for sensor2\n",
      "result = {'trigger_type': 'timeout'}\n"
     ]
    }
   ],
   "source": [
    "# reset both sensors\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"off\"\n",
    "# this will trigger the function\n",
    "input_boolean.test_sensor1 = \"on\"\n",
    "# wait a few seconds, then turn sensor2 on:\n",
    "task.sleep(12)\n",
    "input_boolean.test_sensor2 = \"on\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now the result shows that the timeout caused `task.wait_until` to return."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now assume we also want to make sure that sensor1 stays on the whole time we are waiting. We now check the return values and display the three different cases:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [],
   "source": [
    "@state_trigger(\"input_boolean.test_sensor1 == 'on'\")\n",
    "def sensor1_active():\n",
    "    task.unique('sensor1_active')\n",
    "    log.debug(\"waiting for sensor2\")\n",
    "    result = task.wait_until(\n",
    "        state_trigger=\"input_boolean.test_sensor2 == 'on' or input_boolean.test_sensor1 == 'off'\",\n",
    "        timeout=10\n",
    "    )\n",
    "    if result[\"trigger_type\"] == \"timeout\":\n",
    "        log.debug(f\"sensor2 didn't turn on within 10 seconds\")\n",
    "    elif result[\"trigger_type\"] == \"state\":\n",
    "        if result[\"var_name\"] == \"input_boolean.test_sensor2\":\n",
    "            log.debug(f\"sensor2 turned on within 10 seconds of sensor1 turning on\")\n",
    "        else:\n",
    "            log.debug(f\"sensor1 turned off before sensor2 turned on\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's test it out. In the first, normal, case, sensor2 turns 3 seconds after sensor1:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "waiting for sensor2\n",
      "sensor2 turned on within 10 seconds of sensor1 turning on\n"
     ]
    }
   ],
   "source": [
    "# reset both sensors\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"off\"\n",
    "# this will trigger the function\n",
    "input_boolean.test_sensor1 = \"on\"\n",
    "# wait a few seconds, then turn sensor2 on:\n",
    "task.sleep(3)\n",
    "input_boolean.test_sensor2 = \"on\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's turn sensor1 back off before sensor2: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "waiting for sensor2\n",
      "sensor1 turned off before sensor2 turned on\n"
     ]
    }
   ],
   "source": [
    "# reset both sensors\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"off\"\n",
    "# this will trigger the function\n",
    "input_boolean.test_sensor1 = \"on\"\n",
    "# wait a few seconds, then turn sensor1 off immediately before sensor2:\n",
    "task.sleep(3)\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"on\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, let's confirm the timeout case works too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "waiting for sensor2\n",
      "sensor2 didn't turn on within 10 seconds\n"
     ]
    }
   ],
   "source": [
    "# reset both sensors\n",
    "input_boolean.test_sensor1 = \"off\"\n",
    "input_boolean.test_sensor2 = \"off\"\n",
    "# this will trigger the function\n",
    "input_boolean.test_sensor1 = \"on\"\n",
    "# wait for a longer time, then turn sensor2 on:\n",
    "task.sleep(12)\n",
    "input_boolean.test_sensor2 = \"on\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's clean up the function before moving on."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "outputs": [],
   "source": [
    "del sensor1_active"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Global Contexts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First, you normally shouldn't have to think or care about global contexts, so this part of the tutorial is only if you are really interested. You should first read the documentation to understand what they do.",
    "\n",
    "You can list all the global contexts.  There is one for each pyscript script file, and one for each Jupyter session.  Our session is listed first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['jupyter_0', 'file.scripts']"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pyscript.list_global_ctx()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can get the current global context:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'jupyter_0'"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pyscript.get_global_ctx()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and set it: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [],
   "source": [
    "pyscript.set_global_ctx(\"file.scripts\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's set a global variable in this context, and switch back to our original one:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_in_file_scripts = 123"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "123"
      ]
     },
     "execution_count": 62,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x_in_file_scripts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Back in our original global context, we can confirm the variable doesn't exist:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'x_in_file_scripts' is not defined",
     "output_type": "error",
     "traceback": [
      "Exception in <jupyter_0> line 2:",
      "    x_in_file_scripts",
      "    ^",
      "NameError: name 'x_in_file_scripts' is not defined"
     ]
    }
   ],
   "source": [
    "pyscript.set_global_ctx(\"jupyter_0\")\n",
    "x_in_file_scripts"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "hass pyscript",
   "language": "python",
   "name": "pyscript"
  },
  "language_info": {
   "codemirror_mode": "",
   "file_extension": ".py",
   "mimetype": "",
   "name": "python",
   "nbconvert_exporter": "",
   "version": "1.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
