├── appdaemon ├── py.typed ├── models │ ├── __init__.py │ ├── config │ │ ├── api.py │ │ ├── dashboard.py │ │ ├── http.py │ │ ├── common.py │ │ ├── __init__.py │ │ ├── log.py │ │ └── misc.py │ ├── internal │ │ ├── __init__.py │ │ ├── plugin.py │ │ ├── threading.py │ │ ├── state.py │ │ └── scheduler.py │ └── notification │ │ ├── __init__.py │ │ ├── base.py │ │ └── iOS.py ├── stream │ └── __init__.py ├── plugins │ ├── __init__.py │ ├── hass │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── models.py │ │ └── utils.py │ ├── mqtt │ │ └── __init__.py │ └── dummy │ │ └── dummyapi.py ├── assets │ ├── aui │ │ ├── css │ │ │ └── app.css │ │ ├── appdaemon.png │ │ ├── favicon.ico │ │ └── index.html │ ├── images │ │ ├── Blank.gif │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-70x70.png │ │ ├── apple-touch-icon.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ └── manifest.json │ ├── css │ │ ├── zen │ │ │ └── img │ │ │ │ ├── zen_bg.jpg │ │ │ │ ├── zen_bg2.jpg │ │ │ │ └── zen_weatherbg.jpg │ │ ├── glassic │ │ │ └── img │ │ │ │ ├── carbon1.jpg │ │ │ │ ├── carbon2.jpg │ │ │ │ ├── carbon3.jpg │ │ │ │ ├── carbon4.jpg │ │ │ │ └── glassic_bg.jpg │ │ ├── obsidian │ │ │ └── img │ │ │ │ ├── widgetbg.jpg │ │ │ │ ├── obsidian_bg.jpg │ │ │ │ ├── obsidianbg.jpg │ │ │ │ └── obsidian_w_bg.jpg │ │ ├── simplyred │ │ │ ├── img │ │ │ │ ├── goldgradient.png │ │ │ │ └── goldtexture.jpg │ │ │ └── js │ │ │ │ └── timer.js │ │ ├── images │ │ │ ├── ui-icons_444444_256x240.png │ │ │ ├── ui-icons_555555_256x240.png │ │ │ ├── ui-icons_777620_256x240.png │ │ │ ├── ui-icons_777777_256x240.png │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ └── ui-icons_ffffff_256x240.png │ │ ├── climacons-font.css │ │ └── fonts.css │ ├── fonts │ │ ├── repetition.ttf │ │ ├── digital-7-mono.eot │ │ ├── digital-7-mono.ttf │ │ ├── TickingTimebombBB.ttf │ │ ├── climacons-webfont.eot │ │ ├── climacons-webfont.ttf │ │ ├── climacons-webfont.woff │ │ ├── materialdesignicons-webfont.eot │ │ ├── materialdesignicons-webfont.ttf │ │ ├── materialdesignicons-webfont.woff │ │ └── materialdesignicons-webfont.woff2 │ ├── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ ├── fa-solid-900.woff2 │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.woff │ │ └── fa-regular-400.woff2 │ └── templates │ │ ├── body_include.jinja2 │ │ ├── head_include.jinja2 │ │ ├── error.jinja2 │ │ └── logon.jinja2 ├── version.py ├── widgets │ ├── baseradial │ │ ├── baseradial.html │ │ └── baseradial.css │ ├── baseerror │ │ ├── baseerror.html │ │ ├── baseerror.css │ │ └── baseerror.js │ ├── basetemperature │ │ ├── basetemperature.html │ │ └── basetemperature.css │ ├── baseclock │ │ ├── baseclock.html │ │ └── baseclock.css │ ├── radial.yaml │ ├── temperature.yaml │ ├── basegauge │ │ ├── basegauge.html │ │ └── basegauge.css │ ├── baserss │ │ ├── baserss.css │ │ └── baserss.html │ ├── clock.yaml │ ├── baseentitypicture │ │ ├── baseentitypicture.html │ │ ├── baseentitypicture.css │ │ └── baseentitypicture.js │ ├── basecamera │ │ ├── basecamera.html │ │ └── basecamera.css │ ├── camera.yaml │ ├── iframe.yaml │ ├── AdminSummary.yaml │ ├── basejavascript │ │ ├── basejavascript.html │ │ └── basejavascript.css │ ├── basetext │ │ ├── basetext.html │ │ └── basetext.css │ ├── entitypicture.yaml │ ├── baseiframe │ │ ├── baseiframe.html │ │ └── baseiframe.css │ ├── baseicon │ │ ├── baseicon.html │ │ └── baseicon.css │ ├── rss.yaml │ ├── baseselect │ │ ├── baseselect.html │ │ └── baseselect.css │ ├── icon.yaml │ ├── AdminLog.yaml │ ├── baseswitch │ │ ├── baseswitch.html │ │ └── baseswitch.css │ ├── basedatetime │ │ ├── basedatetime.html │ │ └── basedatetime.css │ ├── label.yaml │ ├── AdminTable.yaml │ ├── baseheater │ │ ├── baseheater.html │ │ └── baseheater.css │ ├── basedisplay │ │ ├── basedisplay.html │ │ └── basedisplay.css │ ├── navigate.yaml │ ├── javascript.yaml │ ├── gauge.yaml │ ├── reload.yaml │ ├── input_select.yaml │ ├── baseslider │ │ ├── baseslider.html │ │ └── baseslider.css │ ├── input_text.yaml │ ├── london_underground.yaml │ ├── baseinputnumber │ │ ├── baseinputnumber.html │ │ └── baseinputnumber.css │ ├── basefan │ │ ├── basefan.html │ │ └── basefan.css │ ├── binary_sensor.yaml │ ├── mode.yaml │ ├── input_datetime.yaml │ ├── input_slider.yaml │ ├── sensor.yaml │ ├── climate.yaml │ ├── text_sensor.yaml │ ├── lock.yaml │ ├── baseclimate │ │ ├── baseclimate.html │ │ └── baseclimate.css │ ├── input_number.yaml │ ├── cover.yaml │ ├── switch.yaml │ ├── baselight │ │ ├── baselight.html │ │ └── baselight.css │ ├── script.yaml │ ├── scene.yaml │ ├── input_boolean.yaml │ ├── sequence.yaml │ ├── person.yaml │ ├── heater.yaml │ ├── group.yaml │ ├── light.yaml │ ├── device_tracker.yaml │ ├── weather_summary.yaml │ ├── alarm.yaml │ ├── basemedia │ │ ├── basemedia.html │ │ └── basemedia.css │ ├── basealarm │ │ └── basealarm.css │ ├── fan.yaml │ ├── media_player.yaml │ └── weather.yaml ├── __init__.py ├── admin_loop.py ├── aui │ └── index-prod.html └── futures.py ├── tests ├── __init__.py ├── functional │ ├── __init__.py │ ├── utils.py │ ├── test_startup.py │ ├── test_production_mode.py │ └── test_run_every.py ├── unit │ ├── datetime │ │ ├── __init__.py │ │ └── test_datetime_misc.py │ └── __init__.py ├── conf │ └── apps │ │ ├── hello_world │ │ ├── apps.yaml │ │ └── hello.py │ │ └── apps.yaml ├── ruff.toml └── utils.py ├── .python-version ├── conf ├── apps │ ├── apps.yaml.example │ └── hello.py ├── .gitignore ├── example_dashboards │ └── Modular │ │ ├── clock.yaml │ │ ├── sensor.side_humidity_corrected.yaml │ │ ├── Weather.dash │ │ ├── camera_panel.yaml │ │ ├── Rooms.dash │ │ ├── HelloTest.dash │ │ ├── MainPanel.dash │ │ ├── Controls.dash │ │ ├── Outside.dash │ │ ├── Cameras.dash │ │ ├── Downstairs.dash │ │ ├── OfficePanel.dash │ │ ├── GarageBasement.dash │ │ ├── Doors.dash │ │ ├── System.dash │ │ ├── Upstairs.dash │ │ ├── outside_middle_panel.yaml │ │ ├── Basement.dash │ │ ├── Garage.dash │ │ ├── Hass.dash │ │ ├── downstairs_middle_panel.yaml │ │ ├── upstairs_middle_panel.yaml │ │ ├── bottom_panel.yaml │ │ ├── weather_panel.yaml │ │ ├── mode_panel.yaml │ │ ├── controls_middle_panel.yaml │ │ ├── main_middle_panel.yaml │ │ ├── rooms_panel.yaml │ │ ├── office_middle_panel.yaml │ │ └── Secure.dash ├── example_apps │ ├── ObjectTracker │ │ └── README.md │ ├── globals.py │ ├── eventMonitor.py │ ├── door_notification.py │ ├── motion_notification.py │ ├── outside_lights.py │ ├── momentary_switch.py │ ├── commute.py │ ├── hwcheck.py │ ├── bysykkel.py │ ├── sensor_notify.py │ ├── sensor_notification.py │ ├── ical.py │ ├── grandfather.py │ ├── sequence.py │ ├── minimote.py │ └── motion_lights.py ├── dashboards │ └── Hello.dash └── appdaemon.yaml.example ├── docs ├── images │ ├── fan.png │ ├── rss.png │ ├── alarm.png │ ├── clock.png │ ├── dash.png │ ├── gauge.png │ ├── group.png │ ├── icon.png │ ├── light.png │ ├── list.png │ ├── lock.png │ ├── mode.png │ ├── popup.png │ ├── scene.png │ ├── token.png │ ├── Profile.png │ ├── camera.png │ ├── climate.png │ ├── iframe.png │ ├── navigate.png │ ├── person.png │ ├── radial.png │ ├── reload.png │ ├── script.png │ ├── sensor.png │ ├── sequence.png │ ├── switch.png │ ├── weather.png │ ├── input_text.png │ ├── javascript.png │ ├── text_label.png │ ├── binary_sensor.png │ ├── create_token.png │ ├── input_boolean.png │ ├── input_number.png │ ├── input_select.png │ ├── input_slider.png │ ├── media_player.png │ ├── temperature.png │ ├── device_tracker.png │ ├── input_datetime.png │ ├── weather_summary.png │ ├── pycharm-run-module.png │ └── pycharm-disable-add-source-roots.png ├── _templates │ └── layout.html ├── css │ └── tablefix.css ├── UPGRADE_FROM_3.x.rst ├── REST_STREAM_API.rst ├── COMMUNITY_TUTORIALS.rst └── conf.py ├── MANIFEST.in ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature_request.yaml ├── dependabot.yml └── workflows │ ├── codespell.yml │ └── stale-issues.yml ├── .readthedocs.yaml ├── .vscode ├── settings.json └── launch.json ├── .pre-commit-config.yaml ├── scripts └── docker-build.sh ├── README.rst ├── README.md ├── .gitignore └── CLA.md /appdaemon/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /appdaemon/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appdaemon/stream/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appdaemon/models/config/api.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appdaemon/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/datetime/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /appdaemon/assets/aui/css/app.css: -------------------------------------------------------------------------------- 1 | .v-card__text{color:#fff!important} 2 | -------------------------------------------------------------------------------- /appdaemon/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.5.13" 2 | __version_comments__ = "" 3 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseradial/baseradial.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /conf/apps/apps.yaml.example: -------------------------------------------------------------------------------- 1 | hello_world: 2 | module: hello 3 | class: HelloWorld 4 | -------------------------------------------------------------------------------- /docs/images/fan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/fan.png -------------------------------------------------------------------------------- /docs/images/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/rss.png -------------------------------------------------------------------------------- /appdaemon/widgets/baseerror/baseerror.html: -------------------------------------------------------------------------------- 1 |

2 | -------------------------------------------------------------------------------- /appdaemon/widgets/basetemperature/basetemperature.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/alarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/alarm.png -------------------------------------------------------------------------------- /docs/images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/clock.png -------------------------------------------------------------------------------- /docs/images/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/dash.png -------------------------------------------------------------------------------- /docs/images/gauge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/gauge.png -------------------------------------------------------------------------------- /docs/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/group.png -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/icon.png -------------------------------------------------------------------------------- /docs/images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/light.png -------------------------------------------------------------------------------- /docs/images/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/list.png -------------------------------------------------------------------------------- /docs/images/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/lock.png -------------------------------------------------------------------------------- /docs/images/mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/mode.png -------------------------------------------------------------------------------- /docs/images/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/popup.png -------------------------------------------------------------------------------- /docs/images/scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/scene.png -------------------------------------------------------------------------------- /docs/images/token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/token.png -------------------------------------------------------------------------------- /docs/images/Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/Profile.png -------------------------------------------------------------------------------- /docs/images/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/camera.png -------------------------------------------------------------------------------- /docs/images/climate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/climate.png -------------------------------------------------------------------------------- /docs/images/iframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/iframe.png -------------------------------------------------------------------------------- /docs/images/navigate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/navigate.png -------------------------------------------------------------------------------- /docs/images/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/person.png -------------------------------------------------------------------------------- /docs/images/radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/radial.png -------------------------------------------------------------------------------- /docs/images/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/reload.png -------------------------------------------------------------------------------- /docs/images/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/script.png -------------------------------------------------------------------------------- /docs/images/sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/sensor.png -------------------------------------------------------------------------------- /docs/images/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/sequence.png -------------------------------------------------------------------------------- /docs/images/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/switch.png -------------------------------------------------------------------------------- /docs/images/weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/weather.png -------------------------------------------------------------------------------- /docs/images/input_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_text.png -------------------------------------------------------------------------------- /docs/images/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/javascript.png -------------------------------------------------------------------------------- /docs/images/text_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/text_label.png -------------------------------------------------------------------------------- /docs/images/binary_sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/binary_sensor.png -------------------------------------------------------------------------------- /docs/images/create_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/create_token.png -------------------------------------------------------------------------------- /docs/images/input_boolean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_boolean.png -------------------------------------------------------------------------------- /docs/images/input_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_number.png -------------------------------------------------------------------------------- /docs/images/input_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_select.png -------------------------------------------------------------------------------- /docs/images/input_slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_slider.png -------------------------------------------------------------------------------- /docs/images/media_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/media_player.png -------------------------------------------------------------------------------- /docs/images/temperature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/temperature.png -------------------------------------------------------------------------------- /docs/images/device_tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/device_tracker.png -------------------------------------------------------------------------------- /docs/images/input_datetime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/input_datetime.png -------------------------------------------------------------------------------- /docs/images/weather_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/weather_summary.png -------------------------------------------------------------------------------- /appdaemon/assets/aui/appdaemon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/aui/appdaemon.png -------------------------------------------------------------------------------- /appdaemon/assets/aui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/aui/favicon.ico -------------------------------------------------------------------------------- /appdaemon/assets/images/Blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/Blank.gif -------------------------------------------------------------------------------- /conf/.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | # avoid commitinng configuration files used during development of Docker build 3 | *.yaml 4 | -------------------------------------------------------------------------------- /docs/images/pycharm-run-module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/pycharm-run-module.png -------------------------------------------------------------------------------- /appdaemon/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/favicon.ico -------------------------------------------------------------------------------- /appdaemon/models/internal/__init__.py: -------------------------------------------------------------------------------- 1 | """This sub-package contains pydantic models for internal parts of AppAdaemon 2 | """ 3 | -------------------------------------------------------------------------------- /appdaemon/assets/css/zen/img/zen_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/zen/img/zen_bg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/fonts/repetition.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/repetition.ttf -------------------------------------------------------------------------------- /appdaemon/assets/css/zen/img/zen_bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/zen/img/zen_bg2.jpg -------------------------------------------------------------------------------- /appdaemon/assets/fonts/digital-7-mono.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/digital-7-mono.eot -------------------------------------------------------------------------------- /appdaemon/assets/fonts/digital-7-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/digital-7-mono.ttf -------------------------------------------------------------------------------- /appdaemon/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /appdaemon/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /appdaemon/assets/images/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/mstile-70x70.png -------------------------------------------------------------------------------- /appdaemon/assets/css/glassic/img/carbon1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/glassic/img/carbon1.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/glassic/img/carbon2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/glassic/img/carbon2.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/glassic/img/carbon3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/glassic/img/carbon3.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/glassic/img/carbon4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/glassic/img/carbon4.jpg -------------------------------------------------------------------------------- /appdaemon/assets/fonts/TickingTimebombBB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/TickingTimebombBB.ttf -------------------------------------------------------------------------------- /appdaemon/assets/fonts/climacons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/climacons-webfont.eot -------------------------------------------------------------------------------- /appdaemon/assets/fonts/climacons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/climacons-webfont.ttf -------------------------------------------------------------------------------- /appdaemon/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /appdaemon/assets/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/mstile-144x144.png -------------------------------------------------------------------------------- /appdaemon/assets/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/mstile-150x150.png -------------------------------------------------------------------------------- /appdaemon/assets/images/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/mstile-310x150.png -------------------------------------------------------------------------------- /appdaemon/assets/images/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/mstile-310x310.png -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /appdaemon/assets/css/obsidian/img/widgetbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/obsidian/img/widgetbg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/zen/img/zen_weatherbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/zen/img/zen_weatherbg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/fonts/climacons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/climacons-webfont.woff -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /appdaemon/assets/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /appdaemon/plugins/hass/__init__.py: -------------------------------------------------------------------------------- 1 | from .hassapi import Hass 2 | from .hassplugin import HassPlugin 3 | 4 | __all__ = ["Hass", "HassPlugin"] 5 | -------------------------------------------------------------------------------- /appdaemon/plugins/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | from .mqttapi import Mqtt 2 | from .mqttplugin import MqttPlugin 3 | 4 | __all__ = ["Mqtt", "MqttPlugin"] 5 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseerror/baseerror.css: -------------------------------------------------------------------------------- 1 | 2 | .widget-baseerror-{{id}} .error { 3 | position: absolute; 4 | top: 5px; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /appdaemon/assets/css/glassic/img/glassic_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/glassic/img/glassic_bg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/obsidian/img/obsidian_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/obsidian/img/obsidian_bg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/obsidian/img/obsidianbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/obsidian/img/obsidianbg.jpg -------------------------------------------------------------------------------- /docs/images/pycharm-disable-add-source-roots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/docs/images/pycharm-disable-add-source-roots.png -------------------------------------------------------------------------------- /appdaemon/assets/css/obsidian/img/obsidian_w_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/obsidian/img/obsidian_w_bg.jpg -------------------------------------------------------------------------------- /appdaemon/assets/css/simplyred/img/goldgradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/simplyred/img/goldgradient.png -------------------------------------------------------------------------------- /appdaemon/assets/css/simplyred/img/goldtexture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/simplyred/img/goldtexture.jpg -------------------------------------------------------------------------------- /appdaemon/assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /appdaemon/assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /appdaemon/assets/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /appdaemon/assets/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/css/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/css/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /appdaemon/assets/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /appdaemon/assets/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppDaemon/appdaemon/HEAD/appdaemon/assets/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """This package has unit tests for AppDaemon, which are considered to be tests that don't actually require AppDaemon to 2 | be running. 3 | """ 4 | -------------------------------------------------------------------------------- /tests/conf/apps/hello_world/apps.yaml: -------------------------------------------------------------------------------- 1 | hello_world: 2 | module: hello 3 | class: HelloWorld 4 | 5 | another_app: 6 | module: hello 7 | class: HelloWorld 8 | my_kwarg: "asdf" 9 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseclock/baseclock.html: -------------------------------------------------------------------------------- 1 |

2 |

3 | -------------------------------------------------------------------------------- /appdaemon/widgets/basetemperature/basetemperature.css: -------------------------------------------------------------------------------- 1 | .widget-basetemperature-{{id}} .canvasclass { 2 | position: relative; 3 | vertical-align: middle; 4 | horizontal-align: middle; 5 | } 6 | -------------------------------------------------------------------------------- /appdaemon/widgets/radial.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseradial 2 | entity: "{{entity}}" 3 | fields: [] 4 | static_css: 5 | widget_style: $radial_widget_style 6 | css: [] 7 | icons: [] 8 | static_icons: [] 9 | -------------------------------------------------------------------------------- /appdaemon/assets/templates/body_include.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if body_includes is defined %} 4 | {% for item in body_includes %} 5 | {{ item }} 6 | {% endfor %} 7 | {%endif%} 8 | -------------------------------------------------------------------------------- /appdaemon/assets/templates/head_include.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if head_includes is defined %} 4 | {% for item in head_includes %} 5 | {{ item }} 6 | {% endfor %} 7 | {%endif%} 8 | -------------------------------------------------------------------------------- /appdaemon/models/notification/__init__.py: -------------------------------------------------------------------------------- 1 | from .android import AndroidData, AndroidPayload 2 | from .iOS import iOSData, iOSPayload 3 | 4 | __all__ = ['AndroidData', 'AndroidPayload', 'iOSData', 'iOSPayload'] 5 | -------------------------------------------------------------------------------- /appdaemon/widgets/temperature.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basetemperature 2 | entity: "{{entity}}" 3 | fields: [] 4 | static_css: 5 | widget_style: $thermo_widget_style 6 | css: [] 7 | icons: [] 8 | static_icons: [] 9 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/clock.yaml: -------------------------------------------------------------------------------- 1 | widget_type: clock 2 | #time_format: 24hr 3 | #show_seconds: 1 4 | widget_style: "background: white" 5 | date_style: "color: black" 6 | time_style: "color: green" 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include README.md 3 | 4 | recursive-include appdaemon/assets * 5 | recursive-include appdaemon/widgets * 6 | 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/sensor.side_humidity_corrected.yaml: -------------------------------------------------------------------------------- 1 | widget_type: sensor 2 | title: Humidity 3 | units: "%" 4 | precision: 0 5 | background_color: yellow 6 | entity: sensor.side_humidity_corrected 7 | -------------------------------------------------------------------------------- /appdaemon/widgets/basegauge/basegauge.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |
5 | -------------------------------------------------------------------------------- /conf/example_apps/ObjectTracker/README.md: -------------------------------------------------------------------------------- 1 | ***Object Tracker 2.0*** 2 | 3 | Contributed by ReneTode. 4 | 5 | For information on how to configure and use see https://community.home-assistant.io/t/objecttracker-2-0-trough-appdaemon/3573 6 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseclock/baseclock.css: -------------------------------------------------------------------------------- 1 | .widget-baseclock-{{id}}.date { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | .widget-baseclock-{{id}}.time { 7 | position: absolute; 8 | top: 45px; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /appdaemon/widgets/baserss/baserss.css: -------------------------------------------------------------------------------- 1 | .widget-baserss-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baserss-{{id}} .title2 { 8 | position: absolute; 9 | top: 23px; 10 | width: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /appdaemon/__init__.py: -------------------------------------------------------------------------------- 1 | from .adapi import ADAPI 2 | from .appdaemon import AppDaemon 3 | from .plugins.hass.hassapi import Hass 4 | from .plugins.mqtt.mqttapi import Mqtt 5 | from . import models as cfg 6 | 7 | __all__ = ["ADAPI", "AppDaemon", "Hass", "Mqtt", "cfg"] 8 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Weather.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Weather 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | layout: 10 | - include: weather_panel 11 | - include: bottom_panel 12 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/camera_panel.yaml: -------------------------------------------------------------------------------- 1 | living_room: 2 | widget_type: camera 3 | title: Living Room 4 | refresh: 1 5 | frame_style: "" 6 | entity_picture: !secret cam_url 7 | 8 | layout: 9 | - living_room(4x4) 10 | - 11 | - 12 | - 13 | -------------------------------------------------------------------------------- /appdaemon/widgets/clock.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseclock 2 | fields: 3 | date: "" 4 | time: "" 5 | static_css: 6 | date_style: $clock_date_style 7 | time_style: $clock_time_style 8 | widget_style: $clock_widget_style 9 | static_icons: [] 10 | icons: [] 11 | css: [] 12 | -------------------------------------------------------------------------------- /conf/apps/hello.py: -------------------------------------------------------------------------------- 1 | from appdaemon.adapi import ADAPI 2 | 3 | # 4 | # Hello World App 5 | # 6 | # Args: 7 | # 8 | 9 | 10 | class HelloWorld(ADAPI): 11 | def initialize(self): 12 | self.log("Hello from AppDaemon") 13 | self.log("You are now ready to run Apps!") 14 | -------------------------------------------------------------------------------- /conf/dashboards/Hello.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Hello Panel 5 | widget_dimensions: [120, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | label: 10 | widget_type: label 11 | text: Hello World 12 | 13 | layout: 14 | - label(2x2) 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .pytest_cache 5 | 6 | Dockerfile 7 | .dockerignore 8 | 9 | .pre-commit-config.yaml 10 | 11 | *.md 12 | !LICENSE.md 13 | !README*.md 14 | MANIFEST.in 15 | *.rst 16 | 17 | # Documentation 18 | docs 19 | .readthedocs.yml 20 | 21 | .venv 22 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Rooms.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Rooms 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | layout: 10 | - include: top_panel 11 | - include: rooms_panel 12 | - include: bottom_panel 13 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/HelloTest.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Hello Panel 5 | widget_dimensions: [120, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | label: 10 | widget_type: label 11 | text: Hello World 12 | 13 | layout: 14 | - label(2x2) 15 | -------------------------------------------------------------------------------- /tests/conf/apps/hello_world/hello.py: -------------------------------------------------------------------------------- 1 | from appdaemon.adapi import ADAPI 2 | 3 | 4 | class HelloWorld(ADAPI): 5 | def initialize(self): 6 | self.log("Hello from AppDaemon") 7 | self.log("You are now ready to run Apps!") 8 | self.log(f"My kwarg: {self.args.get('my_kwarg', 'not set')}") 9 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseentitypicture/baseentitypicture.html: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /appdaemon/widgets/basecamera/basecamera.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |

7 | -------------------------------------------------------------------------------- /appdaemon/widgets/camera.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basecamera 2 | fields: 3 | title: "{{title}}" 4 | frame_src: "" 5 | img_src: "" 6 | frame_style: '{{frame_style}}' 7 | icons: [] 8 | static_css: 9 | title_style: $camera_title_style 10 | widget_style: $camera_widget_style 11 | css: [] 12 | static_icons: [] 13 | -------------------------------------------------------------------------------- /appdaemon/widgets/iframe.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseiframe 2 | fields: 3 | title: "{{title}}" 4 | frame_src: "" 5 | img_src: "" 6 | frame_style: '{{frame_style}}' 7 | icons: [] 8 | static_css: 9 | title_style: $iframe_title_style 10 | widget_style: $iframe_widget_style 11 | css: [] 12 | static_icons: [] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a question or need support 4 | url: https://appdaemon.readthedocs.io/en/latest/index.html#assistance 5 | about: We use GitHub for tracking bugs and discuss new features, check our website for resources on getting help. 6 | -------------------------------------------------------------------------------- /appdaemon/assets/images/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /conf/appdaemon.yaml.example: -------------------------------------------------------------------------------- 1 | appdaemon: 2 | latitude: 0 3 | longitude: 0 4 | elevation: 30 5 | time_zone: Europe/Berlin 6 | plugins: 7 | HASS: 8 | type: hass 9 | ha_url: 10 | token: 11 | cert_verify: True 12 | http: 13 | url: http://0.0.0.0:5050 14 | admin: 15 | api: 16 | hadashboard: 17 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "!layout.html" %} 4 | {% set css_files = css_files + ["_static/tablefix.css"] %} 5 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/MainPanel.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Main Panel 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | layout: 10 | - include: top_panel 11 | - include: main_middle_panel 12 | - include: mode_panel 13 | - include: bottom_panel 14 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Controls.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Controls 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | # 9 | # Includes (Optional) 10 | # 11 | layout: 12 | - include: controls_middle_panel 13 | - include: rooms_panel 14 | - include: bottom_panel 15 | -------------------------------------------------------------------------------- /appdaemon/widgets/AdminSummary.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseAdminSummary 2 | entities: {} 3 | fields: 4 | title: "{{title}}" 5 | table: {} 6 | value: "" 7 | static_css: 8 | title_style: $sensor_title_style 9 | widget_style: $sensor_widget_style 10 | container_style: $sensor_container_style 11 | css: [] 12 | icons: [] 13 | static_icons: [] 14 | -------------------------------------------------------------------------------- /appdaemon/widgets/basejavascript/basejavascript.html: -------------------------------------------------------------------------------- 1 | 2 |

3 |

4 |

5 | -------------------------------------------------------------------------------- /appdaemon/widgets/baserss/baserss.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |

4 |

5 | -------------------------------------------------------------------------------- /appdaemon/widgets/basetext/basetext.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Outside.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Outside 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - include: outside_middle_panel 14 | - include: rooms_panel 15 | - include: bottom_panel 16 | -------------------------------------------------------------------------------- /docs/css/tablefix.css: -------------------------------------------------------------------------------- 1 | /* override table width restrictions */ 2 | /* Taken from https://github.com/OpenShot/openshot-qt/pull/1272 */ 3 | .wy-table-responsive table td, .wy-table-responsive table th { 4 | white-space: normal; 5 | } 6 | 7 | .wy-table-responsive { 8 | margin-bottom: 24px; 9 | max-width: 100%; 10 | overflow: visible; 11 | } 12 | -------------------------------------------------------------------------------- /appdaemon/widgets/entitypicture.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseentitypicture 2 | entity: "{{entity}}" 3 | fields: 4 | title: "" 5 | base_url: "" 6 | image_style: "" 7 | 8 | img_inernal_src: "" 9 | img_internal_style: "" 10 | icons: [] 11 | static_css: 12 | title_style: $style_title 13 | widget_style: $background_style 14 | css: [] 15 | static_icons: [] 16 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Cameras.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - include: camera_panel 14 | - include: rooms_panel 15 | - include: bottom_panel 16 | -------------------------------------------------------------------------------- /conf/example_apps/globals.py: -------------------------------------------------------------------------------- 1 | # Global variables 2 | 3 | notify = "ios" 4 | wendy_tracker_id = "dedb5e711a24415baaae5cf8e880d852" 5 | wendy_tracker = "device_tracker.{}".format(wendy_tracker_id) 6 | 7 | # andrew_tracker_id = "andrews_iphone" 8 | andrew_tracker_id = "24dcbba223194e62b7965aa9012b1ad0" 9 | andrew_tracker = "device_tracker.{}".format(andrew_tracker_id) 10 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Downstairs.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Downstairs 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - include: downstairs_middle_panel 14 | - include: rooms_panel 15 | - include: bottom_panel 16 | -------------------------------------------------------------------------------- /appdaemon/widgets/basegauge/basegauge.css: -------------------------------------------------------------------------------- 1 | .widget-basegauge-{{id}} .graph { 2 | position: absolute; 3 | top: 20px; 4 | bottom: 0px; 5 | width: 100%; 6 | } 7 | 8 | .widget-basegauge-{{id}} .title { 9 | position: absolute; 10 | top: 5px; 11 | width: 100%; 12 | } 13 | 14 | .widget-basegauge-{{id}} .title2 { 15 | position: absolute; 16 | top: 23px; 17 | width: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseiframe/baseiframe.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |

8 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseicon/baseicon.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |

4 |

5 | -------------------------------------------------------------------------------- /appdaemon/widgets/rss.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baserss 2 | entity: "{{entity}}" 3 | fields: 4 | title: "{{title}}" 5 | title2: "{{title2}}" 6 | text: "" 7 | description: "" 8 | static_css: 9 | title_style: $rss_title_style 10 | title2_style: $rss_title2_style 11 | text_style: $rss_text_style 12 | widget_style: $rss_widget_style 13 | css: [] 14 | icons: [] 15 | static_icons: [] 16 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | 8 | # Build from the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Install the requirements needed to build the documentation from the appropriate requirements.txt file 13 | python: 14 | install: 15 | - requirements: doc-requirements.txt 16 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseselect/baseselect.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 | 6 |
7 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/OfficePanel.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Office Panel 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - include: top_panel 14 | - include: office_middle_panel 15 | - include: mode_panel 16 | - include: bottom_panel 17 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/GarageBasement.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - include: top_panel 14 | - spacer(8x1) 15 | - spacer(8x1) 16 | - spacer(8x1) 17 | - include: bottom_panel 18 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseradial/baseradial.css: -------------------------------------------------------------------------------- 1 | .widget-baseradial-{{id}} { 2 | position: relative; 3 | border-bottom-left-radius: 50%; 4 | border-bottom-right-radius: 50%; 5 | border-top-left-radius: 50%; 6 | border-top-right-radius: 50% 7 | } 8 | 9 | .widget-baseradial-{{id}} .canvasclass { 10 | position: relative; 11 | vertical-align: middle; 12 | horizontal-align: middle; 13 | } 14 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Doors.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - spacer(8x1) 14 | - spacer(8x1) 15 | - spacer(8x1) 16 | - spacer(8x1) 17 | - include: rooms_panel 18 | - include: bottom_panel 19 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/System.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - spacer(8x1) 14 | - spacer(8x1) 15 | - spacer(8x1) 16 | - spacer(8x1) 17 | - include: rooms_panel 18 | - include: bottom_panel 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/icon.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseicon 2 | entity: "{{entity}}" 3 | fields: 4 | title: "{{title}}" 5 | title2: "{{title2}}" 6 | icon: "" 7 | icon_style: "" 8 | state_text: "" 9 | icons: [] 10 | static_icons: [] 11 | css: [] 12 | static_css: 13 | title_style: $icon_title_style 14 | title2_style: $icon_title2_style 15 | state_text_style: $icon_state_text_style 16 | widget_style: $icon_widget_style 17 | -------------------------------------------------------------------------------- /appdaemon/widgets/AdminLog.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseAdminLog 2 | fields: 3 | entity: "{{entity}}" 4 | title: "{{title}}" 5 | max_loglines: 10 6 | special_css: {} 7 | log_style: "" 8 | logline_style: "" 9 | replace_text: {} 10 | icons: [] 11 | static_css: 12 | widget_style: $sensor_widget_style 13 | title_style: $sensor_title_style 14 | container_style: $sensor_container_style 15 | css: [] 16 | static_icons: [] 17 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseswitch/baseswitch.html: -------------------------------------------------------------------------------- 1 | 2 |

3 |

4 |

5 |

6 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Upstairs.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Upstairs 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | # 9 | # Includes (Optional) 10 | # 11 | includes: 12 | - top_panel 13 | - upstairs_middle_panel 14 | - bottom_panel 15 | 16 | layout: 17 | - include: upstairs_middle_panel 18 | - include: rooms_panel 19 | - include: bottom_panel 20 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/outside_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | porch_light: 2 | widget_type: group 3 | title: Porch 4 | entity: group.porch_light 5 | monitored_entity: light.porch_1 6 | 7 | layout: 8 | - porch_light, light.drive, switch.front_path_switch, switch.outside_decorations_switch 9 | - 10 | - 11 | - scene.porch_on, scene.porch_off, scene.drive_on, scene.drive_off, scene.outside_off, scene.outside_dim, scene.outside_bright 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruff.configuration": "pyproject.toml", 3 | "notebook.defaultFormatter": "charliermarsh.ruff", 4 | "ruff.lineLength": 200, 5 | "python.analysis.typeCheckingMode": "basic", 6 | "python.testing.pytestArgs": [ 7 | "-s", 8 | ], 9 | "python.testing.unittestEnabled": false, 10 | "python.testing.pytestEnabled": true, 11 | "python.testing.autoTestDiscoverOnSavePattern": "tests/**/*.py", 12 | } 13 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Basement.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - binary_sensor.basement_sensor, sensor.basement_smoke 14 | - spacer(8x1) 15 | - spacer(8x1) 16 | - spacer(8x1) 17 | - include: rooms_panel 18 | - include: bottom_panel 19 | -------------------------------------------------------------------------------- /appdaemon/assets/css/climacons-font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Climacons-Font'; 3 | src:url('/fonts/climacons-webfont.eot'); 4 | src:url('/fonts/climacons-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('/fonts/climacons-webfont.svg#Climacons-Font') format('svg'), 6 | url('/fonts/climacons-webfont.woff') format('woff'), 7 | url('/fonts/climacons-webfont.ttf') format('truetype'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Garage.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Garage & Basement 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | #skin = green 9 | # 10 | # Includes (Optional) 11 | # 12 | layout: 13 | - binary_sensor.garage_door_sensor, binary_sensor.garage_sensor 14 | - spacer(8x1) 15 | - spacer(8x1) 16 | - spacer(8x1) 17 | - include: rooms_panel 18 | - include: bottom_panel 19 | -------------------------------------------------------------------------------- /appdaemon/models/config/dashboard.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from .common import BoolNum, CoercedPath 4 | 5 | 6 | class DashboardConfig(BaseModel): 7 | config_dir: CoercedPath | None = None 8 | config_file: CoercedPath | None = None 9 | 10 | dashboard_dir: CoercedPath | None = None 11 | force_compile: BoolNum = False 12 | compile_on_start: BoolNum = False 13 | profile_dashboard: bool = False 14 | dashboard: bool = False 15 | -------------------------------------------------------------------------------- /appdaemon/widgets/basedatetime/basedatetime.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Hass.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Home Assistant 5 | widget_dimensions: [122, 120] 6 | widget_margins: [5, 5] 7 | columns: 8 8 | 9 | ha_frame: 10 | widget_type: iframe 11 | title: Home Assistant 12 | #refresh: 60 13 | url_list: 14 | - http://192.168.1.20:8123 15 | 16 | # 17 | # Includes (Optional) 18 | # 19 | layout: 20 | - ha_frame(8x5) 21 | - 22 | - 23 | - 24 | - 25 | - include: bottom_panel 26 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseentitypicture/baseentitypicture.css: -------------------------------------------------------------------------------- 1 | .widget-baseentitypicture-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baseentitypicture-{{id}} .outer-image { 8 | position: absolute; 9 | top: 23px; 10 | bottom: 0px; 11 | left: 12px; 12 | right: 12px; 13 | z-index: 0; 14 | border: none; 15 | } 16 | 17 | .widget-baseentitypicture-{{id}} .img-frame { 18 | width: 100%; 19 | height: 100%; 20 | border: none; 21 | } 22 | -------------------------------------------------------------------------------- /appdaemon/widgets/label.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedisplay 2 | fields: 3 | title: "{{title}}" 4 | title2: "{{title2}}" 5 | value: "{{text}}" 6 | unit: "" 7 | state_text: "" 8 | static_css: 9 | title_style: $label_title_style 10 | title2_style: $label_title2_style 11 | unit_style: "" 12 | value_style: $label_text_style 13 | state_text_style: $label_state_text_style 14 | widget_style: $label_widget_style 15 | container_style: $label_container_style 16 | css: [] 17 | icons: [] 18 | static_icons: [] 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/AdminTable.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseAdminTable 2 | fields: 3 | title: "{{title}}" 4 | title2: "{{title2}}" 5 | icon: "" 6 | icon_style: "" 7 | tables: [] 8 | table_columns: {} 9 | entity_table_columns: [] 10 | namespaces: [] 11 | show_namespaces: 0 12 | icons: [] 13 | static_css: 14 | widget_style: $switch_widget_style 15 | title_style: $heater_title_style 16 | title2_style: $heater_title2_style 17 | container_style: $heater_container_style 18 | css: [] 19 | static_icons: [] 20 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseheater/baseheater.html: -------------------------------------------------------------------------------- 1 | 2 |

3 |

4 |

5 |
6 | -------------------------------------------------------------------------------- /appdaemon/assets/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Led'; 3 | src: url('fonts/digital-7-mono.eot'); 4 | src: url('fonts/digital-7-mono.eot?#iefix') format('embedded-opentype'), 5 | url('fonts/digital-7-mono.ttf') format('truetype'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'TimeBomb'; 10 | src: url('fonts/TickingTimebombBB.ttf') format('truetype'); 11 | } 12 | 13 | @font-face { 14 | font-family: 'Repetition'; 15 | src: url('fonts/repetition.ttf') format('truetype'); 16 | } 17 | -------------------------------------------------------------------------------- /appdaemon/widgets/basedisplay/basedisplay.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |

5 |

6 |
7 |

8 | -------------------------------------------------------------------------------- /appdaemon/assets/images/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "icons": [ 4 | { 5 | "src": "/images/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/images/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/navigate.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basejavascript 2 | fields: 3 | title: "{{title}}" 4 | title2: "{{title2}}" 5 | icon: "" 6 | icon_style: "" 7 | icons: 8 | icon_active: $navigate_icon_active 9 | icon_inactive: $navigate_icon_inactive 10 | static_css: 11 | title_style: $navigate_title_style 12 | title2_style: $navigate_title2_style 13 | widget_style: $navigate_widget_style 14 | css: 15 | icon_active_style: $navigate_icon_active_style 16 | icon_inactive_style: $navigate_icon_inactive_style 17 | static_icons: [] 18 | -------------------------------------------------------------------------------- /appdaemon/models/config/http.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import BaseModel, HttpUrl, SecretStr 4 | 5 | from .common import CoercedPath 6 | 7 | 8 | class HTTPConfig(BaseModel, extra="allow"): 9 | url: HttpUrl | None = None 10 | password: SecretStr | None = None 11 | transport: Literal["ws", "socketio"] = "ws" 12 | ssl_certificate: CoercedPath | None = None 13 | ssl_key: CoercedPath | None = None 14 | static_dirs: dict[str, CoercedPath] | None = None 15 | headers: dict[str, str] | None = None 16 | -------------------------------------------------------------------------------- /appdaemon/widgets/basejavascript/basejavascript.css: -------------------------------------------------------------------------------- 1 | .widget-basejavascript-{{id}} .icon { 2 | position: absolute; 3 | top: 43px; 4 | width: 100%; 5 | } 6 | 7 | .widget-basejavascript-{{id}} .title { 8 | position: absolute; 9 | top: 5px; 10 | width: 100%; 11 | } 12 | 13 | .widget-basejavascript-{{id}} .title2 { 14 | position: absolute; 15 | top: 23px; 16 | width: 100%; 17 | } 18 | 19 | .widget-basejavascript-{{id}} .toggle-area { 20 | z-index: 10; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /appdaemon/widgets/javascript.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basejavascript 2 | fields: 3 | title: "{{title}}" 4 | title2: "{{title2}}" 5 | icon: "" 6 | icon_style: "" 7 | icons: 8 | icon_active: $javascript_icon_active 9 | icon_inactive: $javascript_icon_inactive 10 | static_css: 11 | title_style: $javascript_title_style 12 | title2_style: $javascript_title2_style 13 | widget_style: $javascript_widget_style 14 | css: 15 | icon_active_style: $javascript_icon_active_style 16 | icon_inactive_style: $javascript_icon_inactive_style 17 | static_icons: [] 18 | -------------------------------------------------------------------------------- /appdaemon/widgets/gauge.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basegauge 2 | entity: "{{entity}}" 3 | low_color: $gauge_low_value_color 4 | med_color: $gauge_med_value_color 5 | high_color: $gauge_high_value_color 6 | bgcolor: $gauge_value_bgcolor 7 | color: $gauge_text_color 8 | fields: 9 | title: "{{title}}" 10 | title2: "{{title2}}" 11 | unit: "" 12 | static_css: 13 | title_style: $gauge_title_style 14 | title2_style: $gauge_title2_style 15 | unit_style: "" 16 | value_style: "" 17 | widget_style: $gauge_widget_style 18 | css: [] 19 | icons: [] 20 | static_icons: [] 21 | -------------------------------------------------------------------------------- /appdaemon/widgets/reload.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basejavascript 2 | command: location.reload(true) 3 | fields: 4 | title: "{{title}}" 5 | title2: "{{title2}}" 6 | icon: "" 7 | icon_style: "" 8 | icons: 9 | icon_active: $reload_icon_active 10 | icon_inactive: $reload_icon_inactive 11 | static_css: 12 | title_style: $reload_title_style 13 | title2_style: $reload_title2_style 14 | widget_style: $reload_widget_style 15 | css: 16 | icon_active_style: $reload_icon_active_style 17 | icon_inactive_style: $reload_icon_inactive_style 18 | static_icons: [] 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/charliermarsh/ruff-pre-commit 8 | rev: 'v0.6.4' 9 | hooks: 10 | - id: ruff 11 | args: [--fix, --exit-non-zero-on-fix] 12 | - repo: https://github.com/codespell-project/codespell 13 | # Configuration for codespell is in pyproject.toml 14 | rev: v2.4.1 15 | hooks: 16 | - id: codespell 17 | additional_dependencies: 18 | - tomli 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_select.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseselect 2 | entity: "{{entity}}" 3 | post_service: 4 | service: input_select/select_option 5 | entity_id: "{{entity}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | inputoptions: [] 10 | selectedoption: "" 11 | icons: [] 12 | css: [] 13 | static_icons: {} 14 | static_css: 15 | title_style: $input_select_title_style 16 | title2_style: $input_select_title2_style 17 | select_style: $input_select_select_style 18 | selectcontainer_style: $input_select_container_style 19 | widget_style: $input_select_widget_style 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for pip 4 | - package-ecosystem: pip 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | 10 | # Maintain dependencies for GitHub Actions 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | 16 | # Enable version updates for Docker 17 | - package-ecosystem: "docker" 18 | # Look for a `Dockerfile` in the `root` directory 19 | directory: "/" 20 | # Check for updates once a week 21 | schedule: 22 | interval: "weekly" 23 | -------------------------------------------------------------------------------- /appdaemon/models/internal/plugin.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class PluginAttributes(BaseModel): 7 | bytes_sent_ps: int = 0 8 | bytes_recv_ps: int = 0 9 | requests_sent_ps: int = 0 10 | updates_recv_ps: int = 0 11 | totalcallbacks: int = 0 12 | instancecallbacks: int = 0 13 | 14 | 15 | class PluginEntity(BaseModel): 16 | """Used by plugin management to call state.add_entity""" 17 | 18 | namespace: str = "admin" 19 | entity: str 20 | state: Any 21 | attributes: PluginAttributes = Field(default_factory=PluginAttributes) 22 | -------------------------------------------------------------------------------- /tests/ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 200 2 | indent-width = 4 3 | 4 | [lint] 5 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 6 | extend-select = ["COM", "E", "F", "UP", "I"] 7 | extend-ignore = ["COM812"] 8 | 9 | # Allow autofix for all enabled rules (when `--fix`) is provided. 10 | extend-fixable = ["E", "F", "UP", "I"] 11 | extend-unfixable = [] 12 | 13 | # Allow unused variables when underscore-prefixed. 14 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 15 | 16 | per-file-ignores = {} 17 | 18 | [lint.mccabe] 19 | max-complexity = 5 20 | 21 | [format] 22 | docstring-code-format = true 23 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseslider/baseslider.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |

5 |

6 |
7 |

8 |

9 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_text.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basetext 2 | entity: "{{entity}}" 3 | fields: 4 | title: "{{title}}" 5 | title2: "{{title2}}" 6 | value: "{{text}}" 7 | unit: "" 8 | state_text: "" 9 | TextValue: "" 10 | static_css: 11 | title_style: $input_text_title_style 12 | title2_style: $input_text_title2_style 13 | text_style: $input_text_text_style 14 | widget_style: $input_text_widget_style 15 | container_style: $input_text_container_style 16 | post_service: 17 | service: input_text/set_value 18 | entity_id: "{{entity}}" 19 | value: "{{value}}" 20 | css: [] 21 | icons: [] 22 | static_icons: [] 23 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | # Codespell configuration is within pyproject.toml 2 | --- 3 | name: Codespell 4 | 5 | on: 6 | push: 7 | branches: [dev] 8 | pull_request: 9 | branches: [dev] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | codespell: 16 | name: Check for spelling errors 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v6 22 | - name: Annotate locations with typos 23 | uses: codespell-project/codespell-problem-matcher@v1 24 | - name: Codespell 25 | uses: codespell-project/actions-codespell@v2 26 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseicon/baseicon.css: -------------------------------------------------------------------------------- 1 | .widget-baseicon-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baseicon-{{id}} .title2 { 8 | position: absolute; 9 | top: 23px; 10 | width: 100%; 11 | } 12 | 13 | .widget-baseicon-{{id}} .state_text { 14 | position: absolute; 15 | bottom: -3px; 16 | width: 100%; 17 | } 18 | 19 | .widget-baseicon-{{id}} .icon { 20 | position: absolute; 21 | top: 43px; 22 | width: 100%; 23 | } 24 | 25 | .widget-baseicon-{{id}} .toggle-area { 26 | z-index: 10; 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | } 33 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/downstairs_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | media_player: 2 | title: Living Room 3 | widget_type: media_player 4 | truncate_name: 35 5 | entity: media_player.living_room 6 | 7 | layout: 8 | - media_player(2x2),binary_sensor.downstairs_sensor 9 | - 10 | - light.kitchen, light.downstairs_hall, light.andrews_lamp, light.wendys_lamp, light.jacks_lamp, light.den_lamp, switch.main_tree_switch, switch.animal_tree_switch 11 | - scene.downstairs_on, scene.downstairs_off, scene.downstairs_dim, scene.downstairs_bright, scene.jacks_heat_lamp, scene.jacks_heat_lamp_off, light.living_room, light.living_room_front 12 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | readonly REPO_DIR=$(cd $(dirname $(dirname $(readlink -f "${BASH_SOURCE[0]}"))) && pwd) 6 | 7 | rm -rf ${REPO_DIR}/build ${REPO_DIR}/dist 8 | 9 | if command -v uv >/dev/null 2>&1; then 10 | uv lock 11 | uv sync --inexact 12 | echo -n "Building wheel..." 13 | uv build --wheel --refresh -q 14 | echo "done." 15 | else 16 | # uv is not installed 17 | echo "uv command not found. See https://docs.astral.sh/uv/getting-started/installation/" 18 | python -m build 19 | fi 20 | 21 | docker build --pull -t acockburn/appdaemon:${1:-"local-dev"} ${REPO_DIR} 22 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseswitch/baseswitch.css: -------------------------------------------------------------------------------- 1 | .widget-baseswitch-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baseswitch-{{id}} .title2 { 8 | position: absolute; 9 | top: 23px; 10 | width: 100%; 11 | } 12 | 13 | .widget-baseswitch-{{id}} .state_text { 14 | position: absolute; 15 | bottom: -3px; 16 | width: 100%; 17 | } 18 | 19 | .widget-baseswitch-{{id}} .icon { 20 | position: absolute; 21 | top: 43px; 22 | width: 100%; 23 | } 24 | 25 | .widget-baseswitch-{{id}} .toggle-area { 26 | z-index: 10; 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | } 33 | -------------------------------------------------------------------------------- /appdaemon/widgets/london_underground.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedisplay 2 | entity: "{{entity}}" 3 | entity_to_sub_entity_attribute: Description 4 | fields: 5 | title: "{{title}}" 6 | title2: "" 7 | value: "" 8 | unit: "" 9 | state_text: "" 10 | static_css: 11 | title_style: $london_underground_title_style 12 | title2_style: "" 13 | unit_style: "" 14 | value_style: "" 15 | state_text_style: $london_underground_state_text_style 16 | widget_style: $london_underground_widget_style 17 | container_style: $london_underground_container_style 18 | 19 | css: 20 | text_style: $london_underground_text_style 21 | icons: [] 22 | static_icons: [] 23 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseinputnumber/baseinputnumber.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |

5 |

6 |

7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature. 3 | labels: ["enhancement"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: Is there an existing feature request for this? 8 | description: Please search to see if there is already a Github issue for the feature you are requesting. 9 | options: 10 | - label: I have searched the existing issues 11 | required: true 12 | - type: textarea 13 | id: feature 14 | attributes: 15 | label: Your feature request 16 | description: How do you think AppDaemon could be improved/what is currently missing? 17 | -------------------------------------------------------------------------------- /appdaemon/widgets/basefan/basefan.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |
5 |
6 |
7 |
8 | 9 | -------------------------------------------------------------------------------- /appdaemon/widgets/binary_sensor.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "on" 4 | state_inactive: "off" 5 | fields: 6 | title: "{{title}}" 7 | title2: "{{title2}}" 8 | icon: "" 9 | icon_style: "" 10 | state_text: "" 11 | icons: 12 | icon_on: $binary_sensor_icon_on 13 | icon_off: $binary_sensor_icon_off 14 | static_icons: [] 15 | css: 16 | icon_style_active: $binary_sensor_icon_style_active 17 | icon_style_inactive: $binary_sensor_icon_style_inactive 18 | static_css: 19 | title_style: $binary_sensor_title_style 20 | title2_style: $binary_sensor_title2_style 21 | state_text_style: $binary_sensor_state_text_style 22 | widget_style: $binary_sensor_widget_style 23 | -------------------------------------------------------------------------------- /appdaemon/widgets/mode.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "{{mode}}" 4 | enable: 1 5 | post_service_active: 6 | service: script/turn_on 7 | entity_id: "{{script}}" 8 | fields: 9 | title: "{{title}}" 10 | title2: "{{title2}}" 11 | icon: "" 12 | icon_style: "" 13 | state_text: "" 14 | icons: 15 | icon_on: $mode_icon_on 16 | icon_off: $mode_icon_off 17 | static_icons: [] 18 | css: 19 | icon_style_active: $mode_icon_style_active 20 | icon_style_inactive: $mode_icon_style_inactive 21 | static_css: 22 | title_style: $mode_title_style 23 | title2_style: $mode_title2_style 24 | state_text_style: $mode_state_text_style 25 | widget_style: $mode_widget_style 26 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_datetime.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedatetime 2 | entity: "{{entity}}" 3 | fields: 4 | title: "{{title}}" 5 | title2: "{{title2}}" 6 | value: "{{text}}" 7 | unit: "" 8 | state_text: "" 9 | DateValue: "" 10 | TimeValue: "" 11 | static_css: 12 | title_style: $input_datetime_title_style 13 | title2_style: $input_datetime_title2_style 14 | unit_style: "" 15 | date_value_style: $input_datetime_date_style 16 | time_value_style: $input_datetime_time_style 17 | widget_style: $input_datetime_widget_style 18 | container_style: $input_datetime_container_style 19 | post_service: 20 | service: input_datetime/set_datetime 21 | entity_id: "{{entity}}" 22 | css: [] 23 | icons: [] 24 | static_icons: [] 25 | -------------------------------------------------------------------------------- /appdaemon/widgets/basetext/basetext.css: -------------------------------------------------------------------------------- 1 | .widget-basetext-{{id}} .value { 2 | display: inline-block; 3 | vertical-align: middle; 4 | } 5 | 6 | .widget-basetext-{{id}} .valueunit { 7 | position: absolute; 8 | width: 100%; 9 | top: 41px; 10 | vertical-align: middle; 11 | } 12 | 13 | .widget-basetext-{{id}} .title { 14 | position: absolute; 15 | top: 5px; 16 | width: 100%; 17 | } 18 | 19 | .widget-basetext-{{id}} .title2 { 20 | position: absolute; 21 | top: 23px; 22 | width: 100%; 23 | } 24 | 25 | .widget-basetext-{{id}} .inputtext { 26 | width: 85%; 27 | height: 16px; 28 | border: 1px solid black; 29 | padding: 2px; 30 | margin: 1px; 31 | text-align: left; 32 | background-color: #999; 33 | } 34 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_slider.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseslider 2 | entity: "{{entity}}" 3 | post_service: 4 | service: input_number/set_value 5 | entity_id: "{{entity}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | unit: "{{unit}}" 10 | level: "" 11 | icons: [] 12 | css: [] 13 | static_icons: 14 | icon_up: $input_slider_icon_up 15 | icon_down: $input_slider_icon_down 16 | static_css: 17 | title_style: $input_slider_title_style 18 | title2_style: $input_slider_title2_style 19 | level_style: $input_slider_level_style 20 | level_up_style: $input_slider_level_up_style 21 | level_down_style: $input_slider_level_down_style 22 | widget_style: $input_slider_widget_style 23 | unit_style: $input_slider_unit_style 24 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/upstairs_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | office_light: 2 | widget_type: group 3 | title: Office 4 | entity: group.office_light 5 | monitored_entity: light.office_1 6 | 7 | upstairs_smoke_alarm: 8 | title: Smoke Alarm 9 | widget_type: sensor 10 | entity: sensor.upstairs_smoke 11 | value_style: "color: white; font-size: 150%;" 12 | 13 | layout: 14 | - upstairs_smoke_alarm, binary_sensor.upstairs_sensor 15 | - 16 | - scene.bedroom_on, scene.bedroom_off, office_light, light.andrew_bedside, light.wendy_bedside, light.upstairs_hall, light.office_lamp 17 | - scene.upstairs_on, scene.upstairs_off, scene.upstairs_hall_dim, scene.upstairs_bright, scene.office_on, scene.office_off, scene.office_dim, scene.office_bright 18 | -------------------------------------------------------------------------------- /appdaemon/widgets/basefan/basefan.css: -------------------------------------------------------------------------------- 1 | .widget-basefan-{{id}} { 2 | position: relative; 3 | } 4 | 5 | .widget-basefan-{{id}} .title { 6 | position: absolute; 7 | top: 5px; 8 | width: 100%; 9 | } 10 | 11 | .widget-basefan-{{id}} .icon { 12 | position: absolute; 13 | top: 26px; 14 | width: 100%; 15 | height: 47px; 16 | } 17 | .widget-basefan-{{id}} .speed1_style { 18 | position: absolute; 19 | bottom: 0px; 20 | width: 33%; 21 | left: 0px; 22 | } 23 | .widget-basefan-{{id}} .speed2_style { 24 | position: absolute; 25 | bottom: 0px; 26 | width: 33%; 27 | left: 33%; 28 | } 29 | .widget-basefan-{{id}} .speed3_style { 30 | position: absolute; 31 | bottom: 0px; 32 | width: 33%; 33 | left: 66%; 34 | } 35 | -------------------------------------------------------------------------------- /conf/example_apps/eventMonitor.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | """ 4 | 5 | Monitor events and output changes to the verbose_log. Nice for debugging purposes. 6 | 7 | Arguments: 8 | - events: List of events to monitor 9 | 10 | """ 11 | 12 | 13 | class Monitor(hass.Hass): 14 | def initialize(self): 15 | events = self.args["events"] 16 | 17 | for event in events: 18 | self.changed(event, None, None, None, None) 19 | 20 | self.log('watching event "{}" for state changes'.format(event)) 21 | self.listen_state(self.changed, event) 22 | 23 | def changed(self, entity, attribute, old, new, kwargs): 24 | value = self.get_state(entity, "all") 25 | self.log(entity + ": " + str(value)) 26 | -------------------------------------------------------------------------------- /appdaemon/models/internal/threading.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Literal 3 | from collections.abc import Callable 4 | 5 | 6 | @dataclass(slots=True) 7 | class StateDispatch: 8 | id: str 9 | name: str 10 | objectid: str 11 | type: str 12 | function: Callable 13 | attribute: str 14 | entity: str 15 | new_state: dict 16 | old_state: dict 17 | pin_app: bool 18 | pin_thread: int 19 | kwargs: dict 20 | 21 | 22 | @dataclass(slots=True) 23 | class EventDispatch: 24 | id: str 25 | name: str 26 | objectid: str 27 | type: Literal["event"] 28 | function: Callable 29 | data: dict[str, Any] 30 | pin_app: bool 31 | pin_thread: int 32 | kwargs: dict[str, Any] 33 | -------------------------------------------------------------------------------- /appdaemon/widgets/sensor.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedisplay 2 | entity: "{{entity}}" 3 | entity_to_sub_entity_attribute: "{{entity_to_sub_entity_attribute}}" 4 | sub_entity: "{{sub_entity}}" 5 | sub_entity_to_entity_attribute: "{{sub_entity_to_entity_attribute}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | value: "" 10 | unit: "" 11 | state_text: "" 12 | static_css: 13 | title_style: $sensor_title_style 14 | title2_style: $sensor_title2_style 15 | state_text_style: $sensor_state_text_style 16 | widget_style: $sensor_widget_style 17 | container_style: $sensor_container_style 18 | css: 19 | value_style: $sensor_value_style 20 | text_style: $sensor_text_style 21 | unit_style: $sensor_unit_style 22 | icons: [] 23 | static_icons: [] 24 | -------------------------------------------------------------------------------- /appdaemon/widgets/basedisplay/basedisplay.css: -------------------------------------------------------------------------------- 1 | .widget-basedisplay-{{id}} .unit { 2 | font-size: 225%; 3 | font-weight: 400; 4 | display: inline-block; 5 | vertical-align: top; 6 | margin-left: 5px; 7 | margin-top: 5px; 8 | } 9 | 10 | .widget-basedisplay-{{id}} .value { 11 | display: inline-block; 12 | vertical-align: middle; 13 | } 14 | 15 | .widget-basedisplay-{{id}} .valueunit { 16 | width: 100%; 17 | vertical-align: middle; 18 | } 19 | 20 | .widget-basedisplay-{{id}} .title { 21 | position: absolute; 22 | top: 5px; 23 | width: 100%; 24 | } 25 | 26 | .widget-basedisplay-{{id}} .title2 { 27 | position: absolute; 28 | top: 23px; 29 | width: 100%; 30 | } 31 | 32 | .widget-basedisplay-{{id}} .state_text { 33 | position: absolute; 34 | bottom: -3px; 35 | width: 100%; 36 | } 37 | -------------------------------------------------------------------------------- /appdaemon/widgets/climate.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseclimate 2 | entity: "{{entity}}" 3 | post_service: 4 | service: climate/set_temperature 5 | entity_id: "{{entity}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | units: "" 10 | level: "" 11 | level2: "" 12 | icons: [] 13 | css: [] 14 | static_icons: 15 | icon_up: $climate_icon_up 16 | icon_down: $climate_icon_down 17 | static_css: 18 | title_style: $climate_title_style 19 | title2_style: $climate_title2_style 20 | level_style: $climate_level_style 21 | level2_style: $climate_level2_style 22 | level_up_style: $climate_level_up_style 23 | level_down_style: $climate_level_down_style 24 | widget_style: $climate_widget_style 25 | unit_style: $climate_unit_style 26 | unit2_style: $climate_unit2_style 27 | -------------------------------------------------------------------------------- /tests/conf/apps/apps.yaml: -------------------------------------------------------------------------------- 1 | state_test_app: 2 | module: state_test_app 3 | class: StateTestApp 4 | delay: 0.5 5 | 6 | scheduler_test_app: 7 | module: scheduler_test_app 8 | class: SchedulerTestApp 9 | 10 | test_run_in: 11 | module: scheduler_test_app 12 | class: TestSchedulerRunIn 13 | 14 | test_event_app: 15 | module: event_test_app 16 | class: TestEventCallback 17 | event: "test_event" 18 | 19 | test_immediate_state: 20 | module: state_test_app 21 | class: TestImmediate 22 | 23 | test_run_daily: 24 | module: scheduler_test_app 25 | class: TestSchedulerRunDaily 26 | time: "00:00:05" 27 | 28 | basic_namespace_app: 29 | module: namespace_app 30 | class: BasicNamespaceTester 31 | 32 | hybrid_namespace_app: 33 | module: namespace_app 34 | class: HybridWritebackTester 35 | -------------------------------------------------------------------------------- /appdaemon/models/config/common.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from pathlib import Path 3 | from typing import Annotated, Literal 4 | 5 | from pydantic import BeforeValidator, PlainSerializer 6 | 7 | from appdaemon.utils import parse_timedelta 8 | 9 | LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 10 | 11 | BoolNum = Annotated[bool, BeforeValidator(lambda v: False if int(v) == 0 else True)] 12 | ParsedTimedelta = Annotated[timedelta, BeforeValidator(parse_timedelta), PlainSerializer(lambda td: td.total_seconds())] 13 | 14 | 15 | CoercedPath = Annotated[Path, BeforeValidator(lambda p: Path(p).resolve())] 16 | CoercedRelPath = Annotated[Path, BeforeValidator(lambda p: Path(p))] 17 | LogPath = Annotated[Literal["STDOUT", "STDERR"], BeforeValidator(lambda s: s.upper())] | CoercedPath 18 | -------------------------------------------------------------------------------- /appdaemon/models/internal/state.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Literal 3 | from pydantic import BaseModel, RootModel 4 | 5 | 6 | class EntityState(BaseModel): 7 | """Used by state.add_entity to store the state""" 8 | 9 | entity_id: str 10 | state: Any 11 | last_changed: datetime | Literal["never"] = "never" 12 | attributes: dict[str, Any] 13 | 14 | @property 15 | def event_entity_add(self) -> dict[str, str | dict]: 16 | return { 17 | "event_type": "__AD_ENTITY_ADDED", 18 | "data": {"entity_id": self.entity_id, "state": self.state}, 19 | } 20 | 21 | 22 | class NamespaceState(RootModel): 23 | root: dict[str, EntityState] 24 | 25 | 26 | class AppDaemonState(RootModel): 27 | root: dict[str, NamespaceState] 28 | -------------------------------------------------------------------------------- /appdaemon/widgets/text_sensor.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedisplay 2 | entity: "{{entity}}" 3 | entity_to_sub_entity_attribute: "{{entity_to_sub_entity_attribute}}" 4 | sub_entity: "{{sub_entity}}" 5 | sub_entity_to_entity_attribute: "{{sub_entity_to_entity_attribute}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | value: "" 10 | unit: "" 11 | state_text: "" 12 | static_css: 13 | title_style: $sensor_title_style 14 | title2_style: $sensor_title2_style 15 | unit_style: "" 16 | value_style: "" 17 | state_text_style: $mode_state_text_style 18 | widget_style: $sensor_widget_style 19 | container_style: $sensor_container_style 20 | css: 21 | value_style: $sensor_value_style 22 | text_style: $sensor_text_style 23 | unit_style: $sensor_unit_style 24 | icons: [] 25 | static_icons: [] 26 | -------------------------------------------------------------------------------- /tests/functional/utils.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from contextlib import AbstractAsyncContextManager 3 | from datetime import timedelta 4 | 5 | import pytest 6 | from appdaemon.appdaemon import AppDaemon 7 | 8 | from tests.utils import assert_timedelta, filter_caplog 9 | 10 | AsyncTempTest = Callable[..., AbstractAsyncContextManager[tuple[AppDaemon, pytest.LogCaptureFixture]]] 11 | 12 | 13 | def check_interval( 14 | caplog: pytest.LogCaptureFixture, 15 | search_str: str, 16 | n: int, 17 | interval: timedelta, 18 | buffer: timedelta = timedelta(microseconds=10000), 19 | ) -> None: 20 | logs = list(filter_caplog(caplog, search_str)) 21 | assert_timedelta(logs, interval, buffer) 22 | assert len(logs) >= n, f"Expected {n} log entries with '{search_str}', found {len(logs)}" 23 | -------------------------------------------------------------------------------- /appdaemon/widgets/lock.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "unlocked" 4 | state_inactive: "locked" 5 | enable: 1 6 | post_service_active: 7 | service: lock/unlock 8 | entity_id: "{{entity}}" 9 | post_service_inactive: 10 | service: lock/lock 11 | entity_id: "{{entity}}" 12 | fields: 13 | title: "{{title}}" 14 | title2: "{{title2}}" 15 | icon: "" 16 | icon_style: "" 17 | state_text: "" 18 | icons: 19 | icon_on: $lock_icon_on 20 | icon_off: $lock_icon_off 21 | static_icons: [] 22 | css: 23 | icon_style_active: $lock_icon_style_active 24 | icon_style_inactive: $lock_icon_style_inactive 25 | static_css: 26 | title_style: $lock_title_style 27 | title2_style: $lock_title2_style 28 | state_text_style: $lock_state_text_style 29 | widget_style: $lock_widget_style 30 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseclimate/baseclimate.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |
4 |

5 |

6 |
7 |
8 |

9 |

10 |
11 |

12 |

13 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseselect/baseselect.css: -------------------------------------------------------------------------------- 1 | .widget-baseselect-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | .widget-baseselect-{{id}} .title2 { 7 | position: absolute; 8 | top: 23px; 9 | width: 100%; 10 | } 11 | 12 | .widget-baseselect-{{id}} .styled-select { 13 | position: absolute; 14 | top: 45px; 15 | height: 29px; 16 | overflow: hidden; 17 | width: 100%; 18 | horizontal-align: center; 19 | margin:auto; 20 | } 21 | 22 | .widget-baseselect-{{id}} .styled-select>select { 23 | background-color: #333; 24 | border: none; 25 | outline: none; 26 | font-size: 16px; 27 | height: 29px; 28 | padding: 5px; 29 | width: 90%; 30 | color: white; 31 | -webkit-border-radius: 5px; 32 | -moz-border-radius: 5px; 33 | border-radius: 5px; 34 | } 35 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseiframe/baseiframe.css: -------------------------------------------------------------------------------- 1 | .widget-baseiframe-{{id}} .title { 2 | position: absolute; 3 | bottom: 0px; 4 | margin: 0px 0px 0px 0px; 5 | width: 100%; 6 | } 7 | 8 | .widget-baseiframe-{{id}} .outer-frame { 9 | position: absolute; 10 | top: 0px; 11 | bottom: 0px; 12 | left: 0px; 13 | right: 0px; 14 | z-index: 1; 15 | border: none; 16 | } 17 | 18 | .widget-baseiframe-{{id}} .outer-image { 19 | position: absolute; 20 | top: 0px; 21 | bottom: 0px; 22 | left: 0px; 23 | right: 0px; 24 | z-index: 0; 25 | border: none; 26 | } 27 | 28 | .widget-baseiframe-{{id}} .scaled-frame { 29 | width: 100%; 30 | height: 100%; 31 | border: none; 32 | } 33 | 34 | .widget-baseiframe-{{id}} .img-frame { 35 | width: 100%; 36 | height: 100%; 37 | border: none; 38 | } 39 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_number.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseinputnumber 2 | entity: "{{entity}}" 3 | post_service: 4 | service: input_number/set_value 5 | entity_id: "{{entity}}" 6 | fields: 7 | title: "{{title}}" 8 | title2: "{{title2}}" 9 | SliderValue: "" 10 | MinValue: "" 11 | MaxValue: "" 12 | sliderValue: "" 13 | minValue: "" 14 | maxValue: "" 15 | StepValue: "" 16 | icons: [] 17 | css: [] 18 | static_icons: [] 19 | static_css: 20 | title_style: $input_number_title_style 21 | title2_style: $input_number_title2_style 22 | minvalue_style: $input_number_minvalue_style 23 | maxvalue_style: $input_number_maxvalue_style 24 | value_style: $input_number_value_style 25 | slider_style: $input_number_slider_style 26 | slidercontainer_style: $input_number_container_style 27 | widget_style: $input_number_widget_style 28 | -------------------------------------------------------------------------------- /appdaemon/widgets/basecamera/basecamera.css: -------------------------------------------------------------------------------- 1 | .widget-basecamera-{{id}} .title { 2 | position: absolute; 3 | bottom: 0px; 4 | margin: 0px 0px 0px 0px; 5 | width: 100%; 6 | } 7 | 8 | .widget-basecamera-{{id}} .outer-frame { 9 | position: absolute; 10 | top: 0px; 11 | bottom: 0px; 12 | left: 0px; 13 | right: 0px; 14 | z-index: 1; 15 | border: none; 16 | } 17 | 18 | .widget-basecamera-{{id}} .outer-image { 19 | position: absolute; 20 | top: 0px; 21 | bottom: 0px; 22 | left: 0px; 23 | right: 0px; 24 | z-index: 0; 25 | border: none; 26 | } 27 | 28 | .widget-basecamera-{{id}} .scaled-frame { 29 | width: 100%; 30 | height: 100%; 31 | border: none; 32 | } 33 | 34 | .widget-basecamera-{{id}} .img-frame { 35 | width: 100%; 36 | height: 100%; 37 | border: none; 38 | } 39 | -------------------------------------------------------------------------------- /appdaemon/widgets/cover.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "open" 4 | state_inactive: "closed" 5 | enable: 1 6 | post_service_active: 7 | service: cover/open_cover 8 | entity_id: "{{entity}}" 9 | post_service_inactive: 10 | service: cover/close_cover 11 | entity_id: "{{entity}}" 12 | fields: 13 | title: "{{title}}" 14 | title2: "{{title2}}" 15 | icon: "" 16 | icon_style: "" 17 | state_text: "" 18 | icons: 19 | icon_on: $cover_icon_on 20 | icon_off: $cover_icon_off 21 | static_icons: [] 22 | css: 23 | icon_style_active: $cover_icon_style_active 24 | icon_style_inactive: $cover_icon_style_inactive 25 | static_css: 26 | title_style: $cover_title_style 27 | title2_style: $cover_title2_style 28 | state_text_style: $cover_state_text_style 29 | widget_style: $cover_widget_style 30 | -------------------------------------------------------------------------------- /appdaemon/widgets/switch.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "on" 4 | state_inactive: "off" 5 | enable: 1 6 | post_service_active: 7 | service: homeassistant/turn_on 8 | entity_id: "{{entity}}" 9 | post_service_inactive: 10 | service: homeassistant/turn_off 11 | entity_id: "{{entity}}" 12 | fields: 13 | title: "{{title}}" 14 | title2: "{{title2}}" 15 | icon: "" 16 | icon_style: "" 17 | state_text: "" 18 | icons: 19 | icon_on: $switch_icon_on 20 | icon_off: $switch_icon_off 21 | static_icons: [] 22 | css: 23 | icon_style_active: $switch_icon_style_active 24 | icon_style_inactive: $switch_icon_style_inactive 25 | static_css: 26 | title_style: $switch_title_style 27 | title2_style: $switch_title2_style 28 | state_text_style: $switch_state_text_style 29 | widget_style: $switch_widget_style 30 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/bottom_panel.yaml: -------------------------------------------------------------------------------- 1 | load_main_panel: 2 | widget_type: navigate 3 | title: Main Panel 4 | icon_inactive: fa-home 5 | dashboard: MainPanel 6 | 7 | load_office_panel: 8 | widget_type: navigate 9 | title: Office Panel 10 | icon_inactive: fa-windows 11 | dashboard: OfficePanel 12 | 13 | load_weather: 14 | widget_type: navigate 15 | title: Weather 16 | icon_inactive: fa-cloud 17 | dashboard: Weather 18 | 19 | load_rooms: 20 | widget_type: navigate 21 | title: Rooms 22 | icon_inactive: fa-bed 23 | dashboard: Rooms 24 | 25 | load_controls: 26 | widget_type: navigate 27 | title: Controls 28 | icon_inactive: fa-dashboard 29 | dashboard: Controls 30 | 31 | layout: 32 | - load_main_panel(2x1), load_office_panel(2x1), load_weather(2x1),load_controls(2x1) 33 | -------------------------------------------------------------------------------- /appdaemon/widgets/baselight/baselight.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |

4 | 5 |

6 |
7 |

8 |

9 |
10 |

11 |

12 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/weather_panel.yaml: -------------------------------------------------------------------------------- 1 | weather_frame: 2 | widget_type: iframe 3 | title: Radar 4 | refresh: 300 5 | frame_style: "" 6 | img_list: 7 | - https://icons.wxug.com/data/weather-maps/radar/united-states/hartford-connecticut-region-current-radar-animation.gif 8 | 9 | 10 | 11 | layout: 12 | - weather_frame(4x5), sensor.dark_sky_minutely_summary(2x1), sensor.dark_sky_hourly_summary(2x1) 13 | - sensor.dark_sky_daily_summary(2x1), sensor.dark_sky_pressure(2x1) 14 | - sensor.dark_sky_temperature, sensor.dark_sky_apparent_temperature, sensor.dark_sky_nearest_storm_distance, sensor.dark_sky_nearest_storm_bearing 15 | - sensor.dark_sky_wind_speed(2x1), sensor.dark_sky_wind_bearing, sensor.dark_sky_humidity 16 | - sensor.dark_sky_visibility, sensor.dark_sky_precip_probability, sensor.dark_sky_precip_intensity(2x1) 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | 9 | 10 | { 11 | "name": "AppDaemon Dev Test", 12 | "type": "debugpy", 13 | "request": "launch", 14 | "module": "appdaemon", 15 | "justMyCode": true, 16 | "args": "-c /home/appdaemon/ad_config/dev_test" 17 | }, 18 | { 19 | "name": "AppDaemon Production", 20 | "type": "debugpy", 21 | "request": "launch", 22 | "module": "appdaemon", 23 | "justMyCode": true, 24 | "args": "-c /home/appdaemon/ad_config/production" 25 | }, 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /appdaemon/widgets/script.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_inactive: "off" 4 | state_active: "on" 5 | enable: 1 6 | momentary: 1000 7 | ignore_state: 1 8 | post_service_active: 9 | service: homeassistant/turn_on 10 | entity_id: "{{entity}}" 11 | post_service_inactive: 12 | service: homeassistant/turn_off 13 | entity_id: "{{entity}}" 14 | fields: 15 | title: "{{title}}" 16 | title2: "{{title2}}" 17 | icon: "" 18 | icon_style: "" 19 | state_text: "" 20 | icons: 21 | icon_on: $script_icon_on 22 | icon_off: $script_icon_off 23 | static_icons: [] 24 | css: 25 | icon_style_active: $script_icon_style_active 26 | icon_style_inactive: $script_icon_style_inactive 27 | static_css: 28 | title_style: $script_title_style 29 | title2_style: $script_title2_style 30 | state_text_style: $script_state_text_style 31 | widget_style: $script_widget_style 32 | -------------------------------------------------------------------------------- /appdaemon/widgets/scene.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_inactive: "scening" 4 | state_active: "stillscening" 5 | enable: 1 6 | momentary: 1000 7 | ignore_state: 1 8 | post_service_active: 9 | service: homeassistant/turn_on 10 | entity_id: "{{entity}}" 11 | post_service_inactive: 12 | service: homeassistant/turn_off 13 | entity_id: "{{entity}}" 14 | fields: 15 | title: "{{title}}" 16 | title2: "{{title2}}" 17 | icon: "" 18 | icon_style: "" 19 | state_text: "" 20 | icons: 21 | icon_on: $scene_icon_on 22 | icon_off: $scene_icon_off 23 | static_icons: [] 24 | css: 25 | icon_style_active: $scene_icon_style_active 26 | icon_style_inactive: $scene_icon_style_inactive 27 | static_css: 28 | title_style: $scene_title_style 29 | title2_style: $scene_title2_style 30 | state_text_style: $scene_state_text_style 31 | widget_style: $scene_widget_style 32 | -------------------------------------------------------------------------------- /appdaemon/widgets/input_boolean.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "on" 4 | state_inactive: "off" 5 | enable: 1 6 | post_service_active: 7 | service: homeassistant/turn_on 8 | entity_id: "{{entity}}" 9 | post_service_inactive: 10 | service: homeassistant/turn_off 11 | entity_id: "{{entity}}" 12 | fields: 13 | title: "{{title}}" 14 | title2: "{{title2}}" 15 | icon: "" 16 | icon_style: "" 17 | state_text: "" 18 | icons: 19 | icon_on: $input_boolean_icon_on 20 | icon_off: $input_boolean_icon_off 21 | static_icons: [] 22 | css: 23 | icon_style_active: $input_boolean_icon_style_active 24 | icon_style_inactive: $input_boolean_icon_style_inactive 25 | static_css: 26 | title_style: $input_boolean_title_style 27 | title2_style: $input_boolean_title2_style 28 | state_text_style: $input_boolean_state_text_style 29 | widget_style: $input_boolean_widget_style 30 | -------------------------------------------------------------------------------- /appdaemon/widgets/sequence.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_inactive: "idle" 4 | state_active: "active" 5 | enable: 1 6 | momentary: 1000 7 | ignore_state: 1 8 | resident_namespace: rules 9 | post_service_active: 10 | service: sequence/run 11 | entity_id: "{{entity}}" 12 | post_service_inactive: 13 | service: sequence/run 14 | entity_id: "{{entity}}" 15 | fields: 16 | title: "{{title}}" 17 | title2: "{{title2}}" 18 | icon: "" 19 | icon_style: "" 20 | state_text: "" 21 | icons: 22 | icon_on: $sequence_icon_on 23 | icon_off: $sequence_icon_off 24 | static_icons: [] 25 | css: 26 | icon_style_active: $sequence_icon_style_active 27 | icon_style_inactive: $sequence_icon_style_inactive 28 | static_css: 29 | title_style: $sequence_title_style 30 | title2_style: $sequence_title2_style 31 | state_text_style: $sequence_state_text_style 32 | widget_style: $sequence_widget_style 33 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseheater/baseheater.css: -------------------------------------------------------------------------------- 1 | .widget-baseheater-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | .widget-baseheater-{{id}} .title2 { 7 | position: absolute; 8 | top: 23px; 9 | width: 100%; 10 | } 11 | 12 | .widget-baseheater-{{id}} .iconcontainer { 13 | position: absolute; 14 | bottom: 25%; 15 | width: 100%; 16 | } 17 | 18 | .widget-baseheater-{{id}} .toggle-area { 19 | z-index: 10; 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 75%; 25 | } 26 | 27 | .widget-baseheater-{{id}} .slidercontainer { 28 | position: absolute; 29 | bottom: 5px; 30 | height: 25%; 31 | overflow: hidden; 32 | width: 100%; 33 | horizontal-align: center; 34 | margin:auto; 35 | } 36 | 37 | .widget-baseheater-{{id}} input[type=range] { 38 | -webkit-appearance: slider-horizontal; 39 | border: 1px solid white; 40 | height: 90%; 41 | width: 90%; 42 | } 43 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/mode_panel.yaml: -------------------------------------------------------------------------------- 1 | morning: 2 | widget_type: mode 3 | icon_on: fa-clock-o 4 | icon_off: fa-clock-o 5 | title: Morning 6 | entity: input_select.house_mode 7 | mode: Morning 8 | script: script.morning 9 | 10 | day: 11 | widget_type: mode 12 | title: Day 13 | icon_on: fa-sun-o 14 | icon_off: fa-sun-o 15 | entity: input_select.house_mode 16 | mode: Day 17 | script: script.day 18 | 19 | evening: 20 | widget_type: mode 21 | title: Evening 22 | icon_on: fa-moon-o 23 | icon_off: fa-moon-o 24 | entity: input_select.house_mode 25 | mode: Evening 26 | script: script.evening 27 | 28 | night: 29 | widget_type: mode 30 | title: Night 31 | icon_on: fa-star-o 32 | icon_off: fa-star-o 33 | entity: input_select.house_mode 34 | mode: Night 35 | script: script.night 36 | 37 | layout: 38 | - morning(2x1), day(2x1), evening(2x1), night(2x1) 39 | -------------------------------------------------------------------------------- /appdaemon/models/config/__init__.py: -------------------------------------------------------------------------------- 1 | """This sub-package contains all the pydantic models for the appdaemon.yaml file. 2 | 3 | Modules: 4 | app: Pydantic models for the app configuration files 5 | appdaemon: Pydantic models for the appdaemon section of the appdaemon.yaml file 6 | common: Common types used in multiple places 7 | http: Pydantic models for the http section of the appdaemon.yaml file 8 | log: Pydantic models for the log section of the appdaemon.yaml file 9 | plugin: Pydantic models for the plugin section of the appdaemon.yaml file 10 | sequence: Pydantic models for the sequences defined in app configuration files 11 | yaml: Top-level pydantic model for the appdaemon.yaml file 12 | """ 13 | 14 | from .app import AllAppConfig, AppConfig, GlobalModule 15 | from .appdaemon import AppDaemonConfig 16 | from .yaml import MainConfig 17 | 18 | __all__ = ["AllAppConfig", "AppConfig", "AppDaemonConfig", "GlobalModule", "MainConfig"] 19 | -------------------------------------------------------------------------------- /appdaemon/widgets/basedatetime/basedatetime.css: -------------------------------------------------------------------------------- 1 | .widget-basedatetime-{{id}} .value { 2 | display: inline-block; 3 | vertical-align: middle; 4 | } 5 | 6 | .widget-basedatetime-{{id}} .valueunit { 7 | position: absolute; 8 | top: 41px; 9 | width: 100%; 10 | vertical-align: middle; 11 | } 12 | 13 | .widget-basedatetime-{{id}} .title { 14 | position: absolute; 15 | top: 5px; 16 | width: 100%; 17 | } 18 | 19 | .widget-basedatetime-{{id}} .title2 { 20 | position: absolute; 21 | top: 23px; 22 | width: 100%; 23 | } 24 | 25 | 26 | .widget-basedatetime-{{id}} input { 27 | width: 85%; 28 | height: 16px; 29 | border: 1px solid black; 30 | padding: 2px; 31 | margin: 1px; 32 | text-align: center; 33 | vertical-align: middle; 34 | background-color: #999; 35 | } 36 | 37 | .widget-basedatetime-{{id}} input ::-webkit-clear-button { 38 | display: none; 39 | -webkit-appearance: none; 40 | margin: 0; 41 | font-size: 0px; 42 | } 43 | -------------------------------------------------------------------------------- /appdaemon/widgets/person.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: "{{entity}}" 3 | state_active: "home" 4 | state_inactive: "not_home" 5 | enable: 0 6 | state_text: 1 7 | post_service_active: 8 | service: state/set 9 | entity_id: "{{entity}}" 10 | state: home 11 | post_service_inactive: 12 | service: state/set 13 | entity_id: "{{entity}}" 14 | state: not_home 15 | fields: 16 | title: "{{title}}" 17 | title2: "{{title2}}" 18 | icon: "" 19 | icon_style: "" 20 | state_text: "" 21 | icons: 22 | icon_on: $person_icon_on 23 | icon_off: $person_icon_off 24 | static_icons: [] 25 | css: 26 | icon_style_active: $person_icon_style_active 27 | icon_style_inactive: $person_icon_style_inactive 28 | static_css: 29 | title_style: $person_title_style 30 | title2_style: $person_title2_style 31 | state_text_style: $person_state_text_style 32 | widget_style: $person_widget_style 33 | state_map: 34 | home: HOME 35 | not_home: AWAY 36 | -------------------------------------------------------------------------------- /tests/unit/datetime/test_datetime_misc.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import pytest 4 | from appdaemon import utils 5 | from pytz import BaseTzInfo 6 | 7 | pytestmark = [ 8 | pytest.mark.ci, 9 | pytest.mark.unit, 10 | ] 11 | 12 | 13 | def test_resolve_offset() -> None: 14 | offsets = sorted(utils.resolve_offset(10, random_start=-5, random_end=5) for _ in range(100)) 15 | assert len(set(offsets)) >= 90, "Offsets should be sufficiently random" 16 | assert offsets[0] > timedelta(seconds=5) 17 | assert offsets[-1] < timedelta(seconds=15) 18 | 19 | 20 | def test_ensure_timezone(tz: BaseTzInfo) -> None: 21 | naive = datetime(2025, 6, 25, 12, 0, 0) 22 | aware = utils.ensure_timezone(naive, tz) 23 | assert aware.tzinfo is not None, "Datetime should be timezone-aware" 24 | assert naive.time() == aware.time(), "Time should remain the same after ensuring timezone" 25 | assert aware != datetime(2025, 6, 25, 12, 0, 0, tzinfo=tz) 26 | -------------------------------------------------------------------------------- /appdaemon/models/notification/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class Action(BaseModel, ABC): 7 | action: str 8 | title: str 9 | uri: str | None = None 10 | 11 | 12 | class Payload(BaseModel, ABC): 13 | group: str | None = None 14 | tag: str | None = None 15 | 16 | 17 | class NotificationData(BaseModel, ABC): 18 | """https://www.home-assistant.io/integrations/notify/#action""" 19 | 20 | title: str | None = None 21 | message: str | None = None 22 | target: str | None = None 23 | data: Payload | None = None 24 | 25 | 26 | class NotifyAction(BaseModel, ABC): 27 | device: str 28 | data: NotificationData = Field(default_factory=NotificationData) 29 | 30 | # def to_kwargs(self) -> dict: 31 | # return self.model_dump(mode='json', exclude_none=True) 32 | 33 | 34 | class Automation(BaseModel): 35 | alias: str | None = None 36 | trigger: str | None = None 37 | action: list[NotifyAction] 38 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseerror/baseerror.js: -------------------------------------------------------------------------------- 1 | function baseerror(widget_id, url, skin, parameters) 2 | { 3 | // Will be using "self" throughout for the various flavors of "this" 4 | // so for consistency ... 5 | 6 | self = this 7 | 8 | // Initialization 9 | 10 | self.widget_id = widget_id 11 | 12 | // Store on brightness or fallback to a default 13 | 14 | // Parameters may come in useful later on 15 | 16 | self.parameters = parameters 17 | 18 | var callbacks = [] 19 | 20 | // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed 21 | // Initial will be called when the dashboard loads and state has been gathered for the entity 22 | // Update will be called every time an update occurs for that entity 23 | 24 | var monitored_entities = [] 25 | 26 | // Finally, call the parent constructor to get things moving 27 | 28 | WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /conf/example_apps/door_notification.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import globals 3 | 4 | # 5 | # App to send notification when door opened or closed 6 | # 7 | # Args: 8 | # 9 | # sensor: sensor to monitor e.g. input_binary.hall 10 | # 11 | # Release Notes 12 | # 13 | # Version 1.0: 14 | # Initial Version 15 | 16 | 17 | class DoorNotification(hass.Hass): 18 | def initialize(self): 19 | if "sensor" in self.args: 20 | for sensor in self.split_device_list(self.args["sensor"]): 21 | self.listen_state(self.state_change, sensor) 22 | else: 23 | self.listen_state(self.motion, "binary_sensor") 24 | 25 | def state_change(self, entity, attribute, old, new, kwargs): 26 | if new == "on" or new == "open": 27 | state = "open" 28 | else: 29 | state = "closed" 30 | self.log("{} is {}".format(self.friendly_name(entity), state)) 31 | self.notify("{} is {}".format(self.friendly_name(entity), state), name=globals.notify) 32 | -------------------------------------------------------------------------------- /appdaemon/widgets/heater.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseheater 2 | icon_entity: "{{icon_entity}}" 3 | slider_entity: "{{slider_entity}}" 4 | post_service_active: 5 | service: homeassistant/turn_on 6 | entity_id: "{{icon_entity}}" 7 | post_service_inactive: 8 | service: homeassistant/turn_off 9 | entity_id: "{{icon_entity}}" 10 | post_service_slider_change: 11 | service: input_slider/select_value 12 | entity_id: "{{slider_entity}}" 13 | fields: 14 | title: "{{title}}" 15 | title2: "{{title2}}" 16 | Temperature: "" 17 | MinValue: "15" 18 | MaxValue: "25" 19 | StepValue: "0.5" 20 | icon: "" 21 | icon_style: "" 22 | icons: 23 | icon_on: $heater_icon_on 24 | icon_off: $heater_icon_off 25 | static_icons: [] 26 | static_css: 27 | title_style: $heater_title_style 28 | title2_style: $heater_title2_style 29 | slider_style: $heater_slider_style 30 | widget_style: $heater_widget_style 31 | css: 32 | icon_style_active: $heater_icon_style_active 33 | icon_style_inactive: $heater_icon_style_inactive 34 | -------------------------------------------------------------------------------- /appdaemon/widgets/group.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baselight 2 | entity: "{{entity}}" 3 | post_service_active: 4 | service: homeassistant/turn_on 5 | entity_id: "{{entity}}" 6 | post_service_inactive: 7 | service: homeassistant/turn_off 8 | entity_id: "{{entity}}" 9 | fields: 10 | title: "{{title}}" 11 | title2: "{{title2}}" 12 | icon: "" 13 | units: "%" 14 | level: "" 15 | state_text: "" 16 | icon_style: "" 17 | icons: 18 | icon_on: $group_icon_on 19 | icon_off: $group_icon_off 20 | static_icons: 21 | icon_up: $group_icon_up 22 | icon_down: $group_icon_down 23 | static_css: 24 | title_style: $group_title_style 25 | title2_style: $group_title2_style 26 | state_text_style: $group_state_text_style 27 | level_style: $group_level_style 28 | unit_style: $group_unit_style 29 | level_up_style: $group_level_up_style 30 | level_down_style: $group_level_down_style 31 | widget_style: $group_widget_style 32 | css: 33 | icon_style_active: $group_icon_style_active 34 | icon_style_inactive: $group_icon_style_inactive 35 | -------------------------------------------------------------------------------- /appdaemon/widgets/light.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baselight 2 | entity: "{{entity}}" 3 | post_service_active: 4 | service: homeassistant/turn_on 5 | entity_id: "{{entity}}" 6 | post_service_inactive: 7 | service: homeassistant/turn_off 8 | entity_id: "{{entity}}" 9 | fields: 10 | title: "{{title}}" 11 | title2: "{{title2}}" 12 | icon: "" 13 | units: "%" 14 | level: "" 15 | state_text: "" 16 | icon_style: "" 17 | icons: 18 | icon_on: $light_icon_on 19 | icon_off: $light_icon_off 20 | static_icons: 21 | icon_up: $light_icon_up 22 | icon_down: $light_icon_down 23 | static_css: 24 | title_style: $light_title_style 25 | title2_style: $light_title2_style 26 | state_text_style: $light_state_text_style 27 | level_style: $light_level_style 28 | unit_style: $light_unit_style 29 | level_up_style: $light_level_up_style 30 | level_down_style: $light_level_down_style 31 | widget_style: $light_widget_style 32 | css: 33 | icon_style_active: $light_icon_style_active 34 | icon_style_inactive: $light_icon_style_inactive 35 | -------------------------------------------------------------------------------- /conf/example_apps/motion_notification.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import globals 3 | 4 | # 5 | # App to send notification when motion detected 6 | # 7 | # Args: 8 | # 9 | # sensor: sensor to monitor e.g. input_binary.hall 10 | # 11 | # Release Notes 12 | # 13 | # Version 1.0: 14 | # Initial Version 15 | 16 | 17 | class MotionNotification(hass.Hass): 18 | def initialize(self): 19 | if "sensor" in self.args: 20 | for sensor in self.split_device_list(self.args["sensor"]): 21 | self.listen_state(self.motion, sensor) 22 | else: 23 | self.listen_state(self.motion, "binary_sensor") 24 | 25 | def motion(self, entity, attribute, old, new, kwargs): 26 | if ("state" in new and new["state"] == "on" and old["state"] == "off") or new == "on": 27 | self.log("Motion detected: {}".format(self.friendly_name(entity))) 28 | self.notify( 29 | "Motion detected: {}".format(self.friendly_name(entity)), 30 | name=globals.notify, 31 | ) 32 | -------------------------------------------------------------------------------- /appdaemon/assets/aui/index.html: -------------------------------------------------------------------------------- 1 | adui
2 | -------------------------------------------------------------------------------- /appdaemon/assets/css/simplyred/js/timer.js: -------------------------------------------------------------------------------- 1 | var now = new Date(); 2 | var hours = now.getHours(); 3 | 4 | //Keep in code - Written by Computerhope.com 5 | //Place this script in your HTML heading section 6 | 7 | document.bgColor="#CC9900"; 8 | 9 | //18-19 night 10 | if (hours > 17 && hours < 20){ 11 | document.write (''); 12 | } 13 | //20-21 night 14 | else if (hours > 19 && hours < 22){ 15 | document.write (''); 16 | } 17 | //22-4 night 18 | else if (hours > 21 || hours < 5){ 19 | document.write (''); 20 | } 21 | //9-17 day 22 | else if (hours > 8 && hours < 18){ 23 | document.write (''); 24 | } 25 | //7-8 day 26 | else if (hours > 6 && hours < 9){ 27 | document.write ('');} 28 | //5-6 day 29 | else if (hours > 4 && hours < 7){ 30 | document.write (''); 31 | } 32 | else { 33 | document.write (''); 34 | } 35 | -------------------------------------------------------------------------------- /appdaemon/widgets/device_tracker.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseswitch 2 | entity: device_tracker.{{device}} 3 | state_active: "home" 4 | state_inactive: "not_home" 5 | enable: 0 6 | state_text: 1 7 | post_service_active: 8 | service: device_tracker/see 9 | dev_id: "{{device}}" 10 | location_name: home 11 | post_service_inactive: 12 | service: device_tracker/see 13 | dev_id: "{{device}}" 14 | location_name: not_home 15 | fields: 16 | title: "{{title}}" 17 | title2: "{{title2}}" 18 | icon: "" 19 | icon_style: "" 20 | state_text: "" 21 | icons: 22 | icon_on: $device_tracker_icon_on 23 | icon_off: $device_tracker_icon_off 24 | static_icons: [] 25 | css: 26 | icon_style_active: $device_tracker_icon_style_active 27 | icon_style_inactive: $device_tracker_icon_style_inactive 28 | static_css: 29 | title_style: $device_tracker_title_style 30 | title2_style: $device_tracker_title2_style 31 | state_text_style: $device_tracker_state_text_style 32 | widget_style: $device_tracker_widget_style 33 | state_map: 34 | home: HOME 35 | not_home: AWAY 36 | -------------------------------------------------------------------------------- /conf/example_apps/outside_lights.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | # 4 | # App to turn lights on and off at sunrise and sunset 5 | # 6 | # Args: 7 | # 8 | # on_scene: scene to activate at sunset 9 | # off_scene: scene to activate at sunrise 10 | 11 | 12 | class OutsideLights(hass.Hass): 13 | def initialize(self): 14 | # Run at Sunrise 15 | self.run_at_sunrise(self.sunrise_cb) 16 | 17 | # Run at Sunset 18 | self.run_at_sunset(self.sunset_cb) 19 | 20 | def sunrise_cb(self, kwargs): 21 | self.log("OutsideLights: Sunrise Triggered") 22 | self.cancel_timers() 23 | self.turn_on(self.args["off_scene"]) 24 | 25 | def sunset_cb(self, kwargs): 26 | self.log("OutsideLights: Sunset Triggered") 27 | self.cancel_timers() 28 | self.turn_on(self.args["on_scene"]) 29 | 30 | def cancel_timers(self): 31 | if "timers" in self.args: 32 | apps = self.args["timers"].split(",") 33 | for app in apps: 34 | App = self.get_app(app) 35 | App.cancel() 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | AppDaemon is a loosely coupled, multi-threaded, sandboxed python 5 | execution environment for writing automation apps for various types of Home Automation Software including `Home 6 | Assistant `__ and MQTT. It has a pluggable architecture allowing it to be integrated with 7 | practically any event driven application. 8 | 9 | It also provides a configurable dashboard (HADashboard) 10 | suitable for wall mounted tablets. 11 | 12 | AppDaemon has reached a very stable point, works reliably and is fairly feature rich at this point 13 | in its development. For that reason, releases have been slow in recent months. This does not mean that AppDaemon has been abandoned - 14 | it is used every day by the core developers and has an active discord server `here `__ - please join us for tips 15 | and tricks, AppDaemon discussions and general home automation. 16 | 17 | For full instructions on installation and use check out the `AppDaemon 18 | Project Documentation `__. 19 | -------------------------------------------------------------------------------- /conf/example_apps/momentary_switch.py: -------------------------------------------------------------------------------- 1 | import ahassapi as hass 2 | import globals 3 | 4 | # 5 | # App to make a regular switch act as a momentary switch 6 | # Args: 7 | # 8 | # switch: switch to make momentary e.g. switch.garage 9 | # delay: amount of time to wait upon activation of the switch before turning it off 10 | # 11 | # 12 | # Release Notes 13 | # 14 | # Version 1.0: 15 | # Initial Version 16 | 17 | 18 | class MomentarySwitch(hass.Hass): 19 | def initialize(self): 20 | self.listen_state(self.state_change, self.args["switch"], new="on") 21 | 22 | def state_change(self, entity, attribute, old, new, kwargs): 23 | self.log_notify("{} turned {}".format(entity, new)) 24 | self.run_in(self.switch_off, self.args["delay"], switch=entity) 25 | 26 | def switch_off(self, kwargs): 27 | self.log_notify("Turning {} off".format(kwargs["switch"])) 28 | self.turn_off(self.args["switch"]) 29 | 30 | def log_notify(self, message): 31 | if "verbose_log" in self.args: 32 | self.log(message) 33 | if "notify" in self.args: 34 | self.notify(message, name=globals.notify) 35 | -------------------------------------------------------------------------------- /conf/example_apps/commute.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | # 4 | # App to send email alert if commute time is too long 5 | # 6 | # Args: 7 | # 8 | # time = time the alert will be sent 9 | # limit = number of minutes over which the alert will be sent 10 | # notify - list of notification services to be notified 11 | # sensor - sensor to get the commute time from 12 | # 13 | # None 14 | # 15 | # Release Notes 16 | # 17 | # Version 1.0: 18 | # Initial Version 19 | 20 | 21 | class Commute(hass.Hass): 22 | def initialize(self): 23 | time = self.parse_time(self.args["time"]) 24 | self.run_daily(self.check_travel, time) 25 | 26 | def check_travel(self, kwargs): 27 | commute = int(self.get_state(self.args["sensor"])) 28 | self.log(commute) 29 | self.log(int(self.args["limit"])) 30 | if commute > int(self.args["limit"]): 31 | message = "Commute warning - current travel time from work to home is {} minutes".format(commute) 32 | self.log(message) 33 | for destination in self.args["notify"]: 34 | self.notify(message, title="Commute Warning", name=destination) 35 | -------------------------------------------------------------------------------- /conf/example_apps/hwcheck.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import globals 3 | 4 | 5 | class HWCheck(hass.Hass): 6 | def initialize(self): 7 | self.listen_event(self.ha_event, "ha_started") 8 | self.listen_event(self.appd_event, "appd_started") 9 | 10 | def ha_event(self, event_name, data, kwargs): 11 | self.log_notify("Home Assistant is up", "INFO") 12 | self.run_in(self.hw_check, self.args["delay"]) 13 | 14 | def appd_event(self, event_name, data, kwargs): 15 | self.log_notify("AppDaemon is up", "INFO") 16 | 17 | def hw_check(self, kwargs): 18 | state = self.get_state() 19 | 20 | if "zwave" in self.args and self.args["zwave"] not in state: 21 | self.log_notify("ZWAVE not started after delay period", "WARNING") 22 | if "hue" in self.args and self.args["hue"] not in state: 23 | self.log_notify("HUE not started after delay period", "WARNING") 24 | 25 | def log_notify(self, msg, level): 26 | self.log(msg, level) 27 | self.notify(msg, name=globals.notify) 28 | 29 | def terminate(self): 30 | self.log("Terminating!", "INFO") 31 | -------------------------------------------------------------------------------- /appdaemon/widgets/weather_summary.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basedisplay 2 | sub_entity: "{{entity}}" 3 | sub_entity_to_entity_attribute: entity_picture 4 | state_map: 5 | "/static/images/darksky/weather-pouring.svg": "" 6 | "/static/images/darksky/weather-snowy.svg": "" 7 | "/static/images/darksky/weather-hail.svg": "" 8 | "/static/images/darksky/weather-windy.svg": "" 9 | "/static/images/darksky/weather-fog.svg": "" 10 | "/static/images/darksky/weather-cloudy.svg": "" 11 | "/static/images/darksky/weather-sunny.svg": "" 12 | "/static/images/darksky/weather-night.svg": "" 13 | "/static/images/darksky/weather-partlycloudy.svg": "" 14 | fields: 15 | title: "{{title}}" 16 | title2: "" 17 | value: "" 18 | unit: "" 19 | state_text: "" 20 | static_css: 21 | title_style: $weather_summary_title_style 22 | title2_style: "" 23 | unit_style: "" 24 | value_style: "" 25 | state_text_style: $weather_summary_state_text_style 26 | widget_style: $weather_summary_widget_style 27 | container_style: $weather_summary_container_style 28 | css: 29 | text_style: $weather_summary_text_style 30 | icons: [] 31 | static_icons: [] 32 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/controls_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | title: Skin Change 3 | title2: Default 4 | widget_type: navigate 5 | dashboard: Controls 6 | skin: default 7 | 8 | glassic: 9 | title: Skin Change 10 | title2: Glassic 11 | widget_type: navigate 12 | dashboard: Controls 13 | skin: glassic 14 | 15 | simplyred: 16 | title: Skin Change 17 | title2: Simply Red 18 | widget_type: navigate 19 | dashboard: Controls 20 | skin: simplyred 21 | 22 | zen: 23 | title: Skin Change 24 | title2: Zen 25 | widget_type: navigate 26 | dashboard: Controls 27 | skin: zen 28 | 29 | obsidian: 30 | title: Skin Change 31 | title2: Obsidian 32 | widget_type: navigate 33 | dashboard: Controls 34 | skin: obsidian 35 | 36 | hass: 37 | widget_type: navigate 38 | title: Home Assistant 39 | icon_inactive: mdi-select-all 40 | dashboard: Hass 41 | 42 | alarm: 43 | widget_type: alarm 44 | entity: alarm_control_panel.ha_alarm 45 | title: Alarm 46 | 47 | layout: 48 | - input_boolean.heating, input_boolean.vacation, input_boolean.motion_notifications 49 | - alarm 50 | - 51 | - default, obsidian, zen, simplyred, glassic, spacer(1x1), hass 52 | -------------------------------------------------------------------------------- /appdaemon/widgets/alarm.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basealarm 2 | entity: "{{entity}}" 3 | initial_string: "Enter Code" 4 | post_service_ah: 5 | service: alarm_control_panel/alarm_arm_home 6 | entity_id: "{{entity}}" 7 | post_service_aa: 8 | service: alarm_control_panel/alarm_arm_away 9 | entity_id: "{{entity}}" 10 | post_service_da: 11 | service: alarm_control_panel/alarm_disarm 12 | entity_id: "{{entity}}" 13 | post_service_tr: 14 | service: alarm_control_panel/alarm_trigger 15 | entity_id: "{{entity}}" 16 | state_map: 17 | pending: Pending 18 | armed_home: Armed Home 19 | armed_away: Armed Away 20 | disarmed: Disarmed 21 | triggered: Triggered 22 | fields: 23 | title: "{{title}}" 24 | title2: "{{title2}}" 25 | state: "" 26 | code: "" 27 | static_css: 28 | title_style: $alarm_title_style 29 | title2_style: $alarm_title2_style 30 | state_style: $alarm_state_style 31 | widget_style: $alarm_widget_style 32 | panel_state_style: $alarm_panel_state_style 33 | panel_code_style: $alarm_panel_code_style 34 | panel_background_style: $alarm_panel_background_style 35 | panel_button_style: $alarm_panel_button_style 36 | css: [] 37 | icons: [] 38 | static_icons: [] 39 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseinputnumber/baseinputnumber.css: -------------------------------------------------------------------------------- 1 | .widget-baseinputnumber-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | .widget-baseinputnumber-{{id}} .title2 { 7 | position: absolute; 8 | top: 23px; 9 | width: 100%; 10 | } 11 | .widget-baseinputnumber-{{id}} .state_text { 12 | position: absolute; 13 | top: 38px; 14 | width: 100%; 15 | } 16 | 17 | .widget-baseinputnumber-{{id}} .minval { 18 | position: absolute; 19 | bottom: 5px; 20 | left: 0px; 21 | width: 33%; 22 | } 23 | .widget-baseinputnumber-{{id}} .maxval { 24 | position: absolute; 25 | bottom: 5px; 26 | right: 0px; 27 | width: 33%; 28 | } 29 | .widget-baseinputnumber-{{id}} .value { 30 | position: absolute; 31 | bottom: 5px; 32 | left: 33%; 33 | width: 33%; 34 | } 35 | 36 | .widget-baseinputnumber-{{id}} .slidercontainer { 37 | position: absolute; 38 | top: 45px; 39 | height: 29px; 40 | overflow: hidden; 41 | width: 100%; 42 | horizontal-align: center; 43 | margin:auto; 44 | } 45 | 46 | .widget-baseinputnumber-{{id}} input[type=range] { 47 | -webkit-appearance: slider-horizontal; 48 | border: 1px solid white; 49 | height: 24px; 50 | width: 90%; 51 | } 52 | -------------------------------------------------------------------------------- /appdaemon/widgets/basemedia/basemedia.html: -------------------------------------------------------------------------------- 1 |

2 |

3 |

4 |

5 |

6 |

7 |

8 |

9 |
10 |

11 |

%

12 |
13 |

14 |

15 | -------------------------------------------------------------------------------- /appdaemon/plugins/dummy/dummyapi.py: -------------------------------------------------------------------------------- 1 | import appdaemon.adapi as adapi 2 | import appdaemon.adbase as adbase 3 | from appdaemon.appdaemon import AppDaemon 4 | from appdaemon.logging import Logging 5 | from appdaemon.models.config.app import AppConfig 6 | 7 | 8 | class Dummy(adbase.ADBase, adapi.ADAPI): 9 | def __init__(self, ad: AppDaemon, config_model: AppConfig): 10 | # Call Super Classes 11 | adbase.ADBase.__init__(self, ad, config_model) 12 | adapi.ADAPI.__init__(self, ad, config_model) 13 | 14 | self.AD = ad 15 | self.config_model = config_model 16 | 17 | self.config = self.AD.config.model_dump(by_alias=True, exclude_unset=True) 18 | self.args = self.config_model.model_dump(by_alias=True, exclude_unset=True) 19 | 20 | self.logger = self._logging.get_child(self.name) 21 | self.err = self._logging.get_error().getChild(self.name) 22 | 23 | @property 24 | def app_config(self): 25 | return self.AD.app_management.app_config 26 | 27 | @property 28 | def global_vars(self): 29 | return self.AD.global_vars 30 | 31 | @property 32 | def _logging(self) -> Logging: 33 | return self.AD.logging 34 | 35 | @property 36 | def name(self) -> str: 37 | return self.config_model.name 38 | -------------------------------------------------------------------------------- /appdaemon/plugins/hass/exceptions.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | 4 | from appdaemon import exceptions as ade 5 | 6 | 7 | @dataclass 8 | class HAConnectionFailure(ade.AppDaemonException): 9 | def __str__(self): 10 | return "Connection to Home Assistant failed" 11 | 12 | 13 | @dataclass 14 | class HAAuthenticationError(ade.AppDaemonException): 15 | pass 16 | 17 | 18 | @dataclass 19 | class HAEventsSubError(ade.AppDaemonException): 20 | code: int 21 | msg: str 22 | 23 | def __str__(self) -> str: 24 | return f"{self.code}: {self.msg}" 25 | 26 | 27 | @dataclass 28 | class HAFailedAuthentication(ade.AppDaemonException): 29 | pass 30 | 31 | 32 | @dataclass 33 | class ScriptNotFound(ade.AppDaemonException): 34 | script_name: str 35 | namespace: str 36 | plugin_name: str 37 | domain: str = field(init=False, default="script") 38 | 39 | def __str__(self): 40 | res = f"'{self.script_name}' not found in plugin '{self.plugin_name}'" 41 | if self.namespace != "default": 42 | res += f" with namespace '{self.namespace}'" 43 | return res 44 | 45 | @dataclass 46 | class HassConnectionError(ade.AppDaemonException): 47 | msg: str 48 | 49 | def __str__(self) -> str: 50 | return self.msg 51 | -------------------------------------------------------------------------------- /appdaemon/models/internal/scheduler.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | from functools import partial 4 | from typing import Literal 5 | 6 | 7 | @dataclass(slots=True) 8 | class ScheduleEntry: 9 | name: str 10 | """Name of the app that registered the callback""" 11 | id: str 12 | """Unique identifier (handle) for the scheduled callback""" 13 | callback: partial = field(repr=False) 14 | """Callable to be executed when the callback is triggered""" 15 | basetime: datetime 16 | """Base time for the callback, without any offset applied""" 17 | timestamp: datetime 18 | """The resolved time when the callback will be executed, including any offset""" 19 | offset: float | None = None 20 | """Offset in seconds to be applied to the base time""" 21 | interval: float | None = None 22 | """Time interval in seconds between executions when it's being restarted""" 23 | repeat: bool = False 24 | """Whether the callback should be restarted after execution""" 25 | type: Literal["next_rising", "next_setting"] | None = None 26 | pin_app: bool | None = None 27 | pin_thread: int | None = None 28 | kwargs: dict = field(default_factory=dict) 29 | 30 | random_start: float | None = None 31 | random_end: float | None = None 32 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/main_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | wlamp: 2 | widget_type: light 3 | title: Wendy's Lamp 4 | entity: light.wendys_lamp 5 | icon_on: mdi-lamp 6 | icon_off: mdi-lamp 7 | on_attributes: 8 | brightness: 255 9 | color_temp: 276 10 | 11 | downstairs_thermometer: 12 | widget_type: climate 13 | title: Downstairs 14 | units: "°F" 15 | entity: climate.downstairs_thermostat_heating_1 16 | 17 | upstairs_thermometer: 18 | widget_type: climate 19 | title: Upstairs 20 | units: "°F" 21 | entity: climate.upstairs_thermostat_heating_1 22 | 23 | basement_thermometer: 24 | widget_type: climate 25 | title: Basement 26 | units: "°F" 27 | entity: climate.basement_thermostat_heating_1 28 | 29 | light_level: 30 | widget_type: sensor 31 | title: Light Level 32 | units: "lux" 33 | precision: 0 34 | shorten: 1 35 | entity: sensor.side_multisensor_luminance 36 | 37 | layout: 38 | - wlamp, scene.downstairs_on, scene.downstairs_off, scene.downstairs_bright, upstairs_thermometer, downstairs_thermometer, basement_thermometer, light_level 39 | - scene.outside_bright, scene.outside_off, scene.porch_on, scene.porch_off, input_boolean.night_outside_motion, input_boolean.guest, input_boolean.cooling, input_boolean.heating 40 | -------------------------------------------------------------------------------- /appdaemon/admin_loop.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from appdaemon.appdaemon import AppDaemon 6 | 7 | 8 | class AdminLoop: 9 | """Called by :meth:`~appdaemon.appdaemon.AppDaemon.register_http`. Loop timed with :attr:`~appdaemon.AppDaemon.admin_delay`""" 10 | 11 | AD: "AppDaemon" 12 | """Reference to the AppDaemon container object 13 | """ 14 | logger: Logger 15 | """Standard python logger named ``AppDaemon._admin_loop`` 16 | """ 17 | name: str = "_admin_loop" 18 | 19 | def __init__(self, ad: "AppDaemon"): 20 | self.AD = ad 21 | self.logger = ad.logging.get_child(self.name) 22 | 23 | async def loop(self): 24 | """Handles calling :meth:`~.threading.Threading.get_callback_update` and :meth:`~.threading.Threading.get_q_update`""" 25 | while not self.AD.stopping: 26 | if ( 27 | self.AD.http is not None 28 | and self.AD.http.stats_update != "none" 29 | and self.AD.sched is not None 30 | ): # fmt: skip 31 | await self.AD.threading.get_callback_update() 32 | await self.AD.threading.get_q_update() 33 | 34 | await self.AD.utility.sleep(self.AD.config.admin_delay.total_seconds(), timeout_ok=True) 35 | -------------------------------------------------------------------------------- /appdaemon/aui/index-prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | adui 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/rooms_panel.yaml: -------------------------------------------------------------------------------- 1 | load_upstairs: 2 | widget_type: navigate 3 | title: Upstairs 4 | icon_inactive: fa-bed 5 | dashboard: Upstairs 6 | 7 | load_garage: 8 | widget_type: navigate 9 | title: Garage 10 | icon_inactive: fa-car 11 | dashboard: Garage 12 | 13 | load_basement: 14 | widget_type: navigate 15 | title: Basement 16 | icon_inactive: mdi-radioactive 17 | dashboard: Basement 18 | 19 | load_downstairs: 20 | widget_type: navigate 21 | title: Downstairs 22 | icon_inactive: fa-tv 23 | dashboard: Downstairs 24 | 25 | load_outside: 26 | widget_type: navigate 27 | title: Outside 28 | icon_inactive: fa-twitter 29 | dashboard: Outside 30 | 31 | load_doors: 32 | widget_type: navigate 33 | title: Doors and Locks 34 | icon_inactive: fa-lock 35 | dashboard: Secure 36 | #args: 37 | # timeout: 3 38 | # return: MainPanel 39 | 40 | load_system: 41 | widget_type: navigate 42 | title: System 43 | icon_inactive: mdi-server 44 | dashboard: System 45 | 46 | load_cameras: 47 | widget_type: navigate 48 | title: Cameras 49 | icon_inactive: mdi-camera 50 | dashboard: Cameras 51 | 52 | layout: 53 | - load_outside, load_upstairs, load_downstairs, load_garage, load_basement, load_doors, load_system, load_cameras 54 | -------------------------------------------------------------------------------- /appdaemon/models/config/log.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from typing import Any 3 | 4 | from pydantic import BaseModel, Field, RootModel 5 | 6 | from .common import LogPath, LogLevel, ParsedTimedelta 7 | 8 | SYSTEM_LOG_NAME_MAP = { 9 | "main_log": 'AppDaemon', 10 | "error_log": 'Error', 11 | "access_log": 'Access', 12 | "diag_log": 'Diag', 13 | } 14 | 15 | 16 | class AppDaemonLogConfig(BaseModel): 17 | filename: LogPath = "STDOUT" 18 | name: str | None = None 19 | level: LogLevel = 'INFO' 20 | log_generations: int = 3 21 | log_size: int = 1000000 22 | format_: str = Field(default="{asctime} {levelname} {appname}: {message}", alias="format") 23 | date_format: str = "%Y-%m-%d %H:%M:%S.%f" 24 | filter_threshold: int = 1 25 | filter_timeout: ParsedTimedelta = timedelta(seconds=0.9) 26 | filter_repeat_delay: ParsedTimedelta = timedelta(seconds=5.0) 27 | 28 | 29 | class AppDaemonFullLogConfig(RootModel): 30 | root: dict[str, AppDaemonLogConfig] = Field(default_factory=dict) 31 | 32 | def model_post_init(self, context: Any) -> None: 33 | if len(self.root) > 0: 34 | for log_name, log_config in self.root.items(): 35 | log_config.name = log_config.name or SYSTEM_LOG_NAME_MAP.get(log_name, None) 36 | if log_config.name is None: 37 | raise NameError(f"Log name must be specified for user logs: {log_name}") 38 | -------------------------------------------------------------------------------- /appdaemon/plugins/hass/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class HAContext(BaseModel): 8 | id: str 9 | parent_id: Optional[str] = None 10 | user_id: Optional[str] = None 11 | 12 | 13 | class ServiceCallData(BaseModel): 14 | domain: str 15 | service: str 16 | service_data: dict 17 | 18 | 19 | class HAState(BaseModel): 20 | entity_id: str 21 | state: str 22 | attributes: dict[str, Any] 23 | last_changed: datetime 24 | last_reported: datetime 25 | last_updated: datetime 26 | context: HAContext 27 | 28 | 29 | class StateChangeData(BaseModel): 30 | entity_id: str 31 | old_state: HAState 32 | new_state: HAState 33 | 34 | 35 | class HAEvent(BaseModel): 36 | event_type: str 37 | data: dict 38 | origin: str 39 | time_fired: datetime 40 | context: HAContext 41 | 42 | @classmethod 43 | def model_validate(cls, kwargs) -> "HAEvent": 44 | event_type = kwargs.get("event_type") 45 | event_class = EVENT_TYPE_MAPPING.get(event_type, cls) 46 | return event_class(**kwargs) 47 | 48 | 49 | class CallServiceEvent(HAEvent): 50 | data: ServiceCallData 51 | 52 | 53 | class StateChangeEvent(HAEvent): 54 | data: StateChangeData 55 | 56 | 57 | EVENT_TYPE_MAPPING = {"call_service": CallServiceEvent, "state_changed": StateChangeEvent} 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | AppDaemon is a loosely coupled, multi-threaded, sandboxed python 4 | execution environment for writing automation apps for various types of Home Automation Software including [Home 5 | Assistant](https://home-assistant.io/) and MQTT. It has a pluggable architecture allowing it to be integrated with 6 | practically any event driven application. 7 | 8 | It also provides a configurable dashboard (HADashboard) 9 | suitable for wall mounted tablets. 10 | 11 | ## Getting started 12 | 13 | For full instructions on installation and usage check out the [AppDaemon Project Documentation](http://appdaemon.readthedocs.io). 14 | 15 | ## Release Cycle Frequency 16 | 17 | AppDaemon has reached a very stable point, works reliably and is fairly feature rich at this point 18 | in its development. For that reason, releases have been slow in recent months. This does not mean that AppDaemon has been abandoned - 19 | it is used every day by the core developers and has an active discord server [here](https://discord.gg/sgSr79jW5x) - please join us for tips 20 | and tricks, AppDaemon discussions and general home automation. 21 | 22 | ## Contributing 23 | 24 | This is an active open-source project. We are always open to people who want to use the code or contribute to it. Thank you for being involved! 25 | Have a look at the [official documentation](https://appdaemon.readthedocs.io/en/latest/DEV.html) for more information. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | docker_build/ 14 | docs_build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | tests/conf/namespaces 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | .docs_build 61 | 62 | # PyBuilder 63 | target/ 64 | .idea/ 65 | 66 | # Python 67 | venv 68 | .venv 69 | uv.lock 70 | *.ipynb 71 | 72 | # Mac stuff 73 | .DS_Store 74 | 75 | # Vim 76 | *~ 77 | 78 | # Ruff 79 | 80 | .ruff_cache 81 | 82 | # IDE 83 | 84 | #.vscode 85 | 86 | # Docs 87 | 88 | .docs_build 89 | 90 | 91 | # Nix devenv 92 | .devenv 93 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/office_middle_panel.yaml: -------------------------------------------------------------------------------- 1 | downstairs_thermometer: 2 | widget_type: climate 3 | title: Downstairs 4 | units: "°F" 5 | entity: climate.downstairs_thermostat_heating_1 6 | 7 | upstairs_thermometer: 8 | widget_type: climate 9 | title: Upstairs 10 | units: "°F" 11 | entity: climate.upstairs_thermostat_heating_1 12 | 13 | basement_thermometer: 14 | widget_type: climate 15 | title: Basement 16 | units: "°F" 17 | entity: climate.basement_thermostat_heating_1 18 | 19 | themostat_setpoint: 20 | widget_type: sensor 21 | title: Thermostat 22 | units: "°F" 23 | precision: 0 24 | entity: sensor.thermostat_set 25 | 26 | garage: 27 | widget_type: switch 28 | title: Garage 29 | entity: switch.garage_door 30 | icon_on: fa-car 31 | icon_off: fa-car 32 | icon_style_active: $style_active_warn 33 | 34 | light_level: 35 | widget_type: sensor 36 | title: Light Level 37 | units: "lux" 38 | precision: 0 39 | shorten: 1 40 | entity: sensor.side_multisensor_luminance 41 | 42 | layout: 43 | - scene.upstairs_on, scene.upstairs_off, scene.upstairs_hall_dim, scene.upstairs_bright, upstairs_thermometer, downstairs_thermometer, basement_thermometer, light_level 44 | - scene.office_on, scene.office_off, scene.office_dim, scene.office_bright, input_boolean.night_outside_motion, input_boolean.guest, input_boolean.cooling, input_boolean.heating 45 | -------------------------------------------------------------------------------- /appdaemon/widgets/basealarm/basealarm.css: -------------------------------------------------------------------------------- 1 | 2 | /*noinspection ALL*/ 3 | .widget-basealarm-{{id}} .state { 4 | display: inline-block; 5 | vertical-align: middle; 6 | } 7 | 8 | /*noinspection ALL*/ 9 | .widget-basealarm-{{id}} .title { 10 | position: absolute; 11 | top: 5px; 12 | width: 100%; 13 | } 14 | 15 | /*noinspection ALL*/ 16 | .widget-basealarm-{{id}} .title2 { 17 | position: absolute; 18 | top: 23px; 19 | width: 100%; 20 | } 21 | 22 | /*noinspection ALL*/ 23 | .widget-basealarm-{{id}} .code { 24 | width: 100%; 25 | } 26 | 27 | /*noinspection ALL*/ 28 | .widget-basealarm-{{id}} .toggle-area { 29 | z-index: 10; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | width: 100%; 34 | height: 100%; 35 | } 36 | 37 | /*noinspection ALL*/ 38 | .widget-basealarm-{{id}} .container { 39 | width: 275px; 40 | display: inline-block; 41 | } 42 | 43 | /*noinspection ALL*/ 44 | .widget-basealarm-{{id}} .panel-state { 45 | font-size: 100%; 46 | } 47 | 48 | /*noinspection ALL*/ 49 | .widget-basealarm-{{id}} .block { 50 | display: inline-block; 51 | width: 75px; 52 | height: 75px; 53 | margin: 5px; 54 | float: top; 55 | font-size: 175%; 56 | text-align: center; 57 | vertical-align: middle; 58 | line-height: 75px; 59 | } 60 | 61 | /*noinspection ALL*/ 62 | .widget-basealarm-{{id}} .block2 { 63 | width: 165px; 64 | } 65 | 66 | /*noinspection ALL*/ 67 | .widget-basealarm-{{id}} .block3 { 68 | width: 255px; 69 | } 70 | -------------------------------------------------------------------------------- /conf/example_apps/bysykkel.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import requests 3 | import json 4 | from datetime import datetime 5 | 6 | """ 7 | 8 | Get availability for Oslo City Bikes 9 | 10 | Arguments: 11 | - event: Entity name when publishing event 12 | - interval: Update interval, in minutes 13 | 14 | """ 15 | 16 | 17 | class Bysykkel(hass.Hass): 18 | def initialize(self): 19 | self.apiUrl = "http://reisapi.ruter.no" 20 | self.entity = self.args["event"] 21 | 22 | now = datetime.now() 23 | interval = int(self.args["interval"]) 24 | 25 | self.run_every(self.updateState, now, interval * 60) 26 | 27 | def fetch(self, path): 28 | res = requests.get(self.apiUrl + path) 29 | return json.loads(res.text) 30 | 31 | def updateState(self, kwargs=None): 32 | status = self.getStatus() 33 | self.set_app_state(self.entity, {"state": "", "attributes": status}) 34 | 35 | def getStatus(self): 36 | stations = self.fetch( 37 | "/Place/GetCityBikeStations?latmin={lat_min}&latmax={lat_max}&longmin={long_min}&longmax={long_max}".format( 38 | **self.args 39 | ) 40 | ) 41 | 42 | return [ 43 | { 44 | "title": s["Title"], 45 | "subtitle": s["Subtitle"], 46 | "bikes": s["Availability"]["Bikes"], 47 | "locks": s["Availability"]["Locks"], 48 | } 49 | for s in stations 50 | ] 51 | -------------------------------------------------------------------------------- /conf/example_apps/sensor_notify.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | # 4 | # App to send notification when sensor values in specific ranges 5 | # Args: 6 | # 7 | # sensor: sensor to monitor e.g. sensor.washer 8 | # range_min: minimum value to regard as 'on' 9 | # range_max: maximum value to regard as 'on' 10 | # verbose_log: if set to anything will verbose_log on and off messages 11 | # verbose_log: if set to anything will notify on and off messages 12 | # 13 | # 14 | # Release Notes 15 | # 16 | # Version 1.0: 17 | # Initial Version 18 | 19 | 20 | class SensorNotification(hass.Hass): 21 | def initialize(self): 22 | self.listen_state(self.state, self.args["sensor"]) 23 | 24 | def in_range(self, value): 25 | if int(self.args["range_min"]) <= int(value) <= int(self.args["range_max"]): 26 | return True 27 | else: 28 | return False 29 | 30 | def state(self, entity, attribute, old, new, kwargs): 31 | if (not self.in_range(old)) and self.in_range(new): 32 | notify = "{} turned on".format(self.friendly_name(entity)) 33 | self.log_notify(notify) 34 | 35 | if self.in_range(old) and (not self.in_range(new)): 36 | notify = "{} turned off".format(self.friendly_name(entity)) 37 | self.log_notify(notify) 38 | 39 | def log_notify(self, message): 40 | if "verbose_log" in self.args: 41 | self.log(message) 42 | if "notify" in self.args: 43 | self.notify(message) 44 | -------------------------------------------------------------------------------- /appdaemon/widgets/baselight/baselight.css: -------------------------------------------------------------------------------- 1 | .widget-baselight-{{id}} .state_text { 2 | font-size: 85%; 3 | } 4 | 5 | .widget-baselight-{{id}} .title { 6 | position: absolute; 7 | top: 5px; 8 | width: 100%; 9 | } 10 | 11 | .widget-baselight-{{id}} .title2 { 12 | position: absolute; 13 | top: 23px; 14 | width: 100%; 15 | } 16 | 17 | .widget-baselight-{{id}} .state_text { 18 | position: absolute; 19 | top: 38px; 20 | width: 100%; 21 | } 22 | 23 | .widget-baselight-{{id}} .icon { 24 | position: absolute; 25 | top: 43px; 26 | width: 100%; 27 | } 28 | 29 | .widget-baselight-{{id}} .toggle-area { 30 | z-index: 10; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 100%; 35 | height: 75%; 36 | } 37 | 38 | .widget-baselight-{{id}} .level { 39 | display: inline-block; 40 | } 41 | 42 | .widget-baselight-{{id}} .unit { 43 | display: inline-block; 44 | } 45 | 46 | .widget-baselight-{{id}} .levelunit { 47 | position: absolute; 48 | bottom: 5px; 49 | width: 100%; 50 | } 51 | 52 | .widget-baselight-{{id}} .secondary-icon { 53 | position: absolute; 54 | bottom: 0px; 55 | font-size: 20px; 56 | width: 32px; 57 | color: white; 58 | } 59 | 60 | .widget-baselight-{{id}} .secondary-icon.plus { 61 | right: 24px; 62 | } 63 | 64 | .widget-baselight-{{id}} .secondary-icon.plus i { 65 | padding-top: 10px; 66 | padding-left: 30px; 67 | } 68 | 69 | .widget-baselight-{{id}} .secondary-icon.minus { 70 | left: 8px; 71 | } 72 | 73 | .widget-baselight-{{id}} .secondary-icon.minus i { 74 | padding-top: 10px; 75 | padding-right: 30px; 76 | } 77 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseslider/baseslider.css: -------------------------------------------------------------------------------- 1 | .widget-baseslider-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baseslider-{{id}} .title2 { 8 | position: absolute; 9 | top: 23px; 10 | width: 100%; 11 | } 12 | 13 | .widget-baseslider-{{id}} .level { 14 | font-size: 250%; 15 | display: inline-block; 16 | } 17 | 18 | .widget-baseslider-{{id}} .unit { 19 | font-size: 100%; 20 | font-weight: 400; 21 | display: inline-block; 22 | vertical-align: top; 23 | margin-left: 5px; 24 | margin-top: 5px; 25 | } 26 | 27 | .widget-baseslider-{{id}} .levelunit { 28 | position: absolute; 29 | top: 43px; 30 | width: 100%; 31 | } 32 | 33 | .widget-baseslider-{{id}} .secondary-icon { 34 | position: absolute; 35 | bottom: 0px; 36 | font-size: 20px; 37 | width: 32px; 38 | color: white; 39 | } 40 | 41 | .widget-baseslider-{{id}} .level2 { 42 | display: inline-block; 43 | } 44 | 45 | .widget-baseslider-{{id}} .unit2 { 46 | font-size: 65%; 47 | font-weight: 400; 48 | display: inline-block; 49 | vertical-align: top; 50 | 51 | } 52 | 53 | .widget-baseslider-{{id}} .levelunit2 { 54 | position: absolute; 55 | bottom: 5px; 56 | width: 100%; 57 | } 58 | .widget-baseslider-{{id}} .secondary-icon.plus { 59 | right: 24px; 60 | } 61 | 62 | .widget-baseslider-{{id}} .secondary-icon.plus i { 63 | padding-top: 10px; 64 | padding-left: 30px; 65 | } 66 | 67 | .widget-baseslider-{{id}} .secondary-icon.minus { 68 | left: 8px; 69 | } 70 | 71 | .widget-baseslider-{{id}} .secondary-icon.minus i { 72 | padding-top: 10px; 73 | padding-right: 30px; 74 | } 75 | -------------------------------------------------------------------------------- /appdaemon/widgets/fan.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basefan 2 | entity: "{{entity}}" 3 | post_service_speed: 4 | service: fan/set_speed 5 | entity_id: "{{entity}}" 6 | post_service_active: 7 | service: fan/turn_on 8 | entity_id: "{{entity}}" 9 | post_service_inactive: 10 | service: fan/turn_off 11 | entity_id: "{{entity}}" 12 | fields: 13 | title: "{{title}}" 14 | icon: "" 15 | icon_style: "" 16 | speed1_style: "" 17 | speed2_style: "" 18 | speed3_style: "" 19 | icon1: "" 20 | icon2: "" 21 | icon3: "" 22 | low_speed: "low" 23 | medium_speed: "medium" 24 | high_speed: "high" 25 | icons: 26 | icon_active: $fan_icon_on 27 | icon_inactive: $fan_icon_off 28 | icon1_active: $fan_speed1_icon_on 29 | icon1_inactive: $fan_speed1_icon_off 30 | icon2_active: $fan_speed2_icon_on 31 | icon2_inactive: $fan_speed2_icon_off 32 | icon3_active: $fan_speed3_icon_on 33 | icon3_inactive: $fan_speed3_icon_off 34 | static_icons: [] 35 | static_css: 36 | title_style: $fan_title_style 37 | widget_style: $fan_widget_style 38 | container_style: $fan_container_style 39 | 40 | css: 41 | icon_style_active: $fan_icon_style_active 42 | icon_style_inactive: $fan_icon_style_inactive 43 | speed1_style_active: $fan_speed1_icon_style_active 44 | speed1_style_inactive: $fan_speed1_icon_style_inactive 45 | speed2_style_active: $fan_speed2_icon_style_active 46 | speed2_style_inactive: $fan_speed2_icon_style_inactive 47 | speed3_style_active: $fan_speed3_icon_style_active 48 | speed3_style_inactive: $fan_speed3_icon_style_inactive 49 | -------------------------------------------------------------------------------- /conf/example_apps/sensor_notification.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import globals 3 | 4 | # 5 | # App to send notification when a sensor changes state 6 | # 7 | # Args: 8 | # 9 | # sensor: sensor to monitor e.g. sensor.upstairs_smoke 10 | # idle_state - normal state of sensor e.g. Idle 11 | # turn_on - scene or device to activate when sensor changes e.g. scene.house_bright 12 | # Release Notes 13 | # 14 | # Version 1.0: 15 | # Initial Version 16 | 17 | 18 | class SensorNotification(hass.Hass): 19 | def initialize(self): 20 | if "sensor" in self.args: 21 | for sensor in self.split_device_list(self.args["sensor"]): 22 | self.listen_state(self.state_change, sensor) 23 | 24 | def state_change(self, entity, attribute, old, new, kwargs): 25 | if new != "": 26 | if "input_select" in self.args: 27 | valid_modes = self.split_device_list(self.args["input_select"]) 28 | select = valid_modes.pop(0) 29 | is_state = self.get_state(select) 30 | else: 31 | is_state = None 32 | valid_modes = () 33 | 34 | self.log("{} changed to {}".format(self.friendly_name(entity), new)) 35 | self.notify( 36 | "{} changed to {}".format(self.friendly_name(entity), new), 37 | name=globals.notify, 38 | ) 39 | if "idle_state" in self.args: 40 | if new != self.args["idle_state"] and "turn_on" in self.args and is_state in valid_modes: 41 | self.turn_on(self.args["turn_on"]) 42 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseclimate/baseclimate.css: -------------------------------------------------------------------------------- 1 | .widget-baseclimate-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-baseclimate-{{id}} .title2 { 8 | position: absolute; 9 | top: 23px; 10 | width: 100%; 11 | } 12 | 13 | .widget-baseclimate-{{id}} .level { 14 | font-size: 250%; 15 | display: inline-block; 16 | } 17 | 18 | .widget-baseclimate-{{id}} .units { 19 | font-size: 100%; 20 | font-weight: 400; 21 | display: inline-block; 22 | vertical-align: top; 23 | margin-left: 5px; 24 | margin-top: 5px; 25 | } 26 | 27 | .widget-baseclimate-{{id}} .levelunits { 28 | position: absolute; 29 | top: 43px; 30 | width: 100%; 31 | } 32 | 33 | .widget-baseclimate-{{id}} .secondary-icon { 34 | position: absolute; 35 | bottom: 0px; 36 | font-size: 20px; 37 | width: 32px; 38 | color: white; 39 | } 40 | 41 | .widget-baseclimate-{{id}} .level2 { 42 | display: inline-block; 43 | } 44 | 45 | .widget-baseclimate-{{id}} .units2 { 46 | font-size: 65%; 47 | font-weight: 400; 48 | display: inline-block; 49 | vertical-align: top; 50 | 51 | } 52 | 53 | .widget-baseclimate-{{id}} .levelunits2 { 54 | position: absolute; 55 | bottom: 5px; 56 | width: 100%; 57 | } 58 | .widget-baseclimate-{{id}} .secondary-icon.plus { 59 | right: 24px; 60 | } 61 | 62 | .widget-baseclimate-{{id}} .secondary-icon.plus i { 63 | padding-top: 10px; 64 | padding-left: 30px; 65 | } 66 | 67 | .widget-baseclimate-{{id}} .secondary-icon.minus { 68 | left: 8px; 69 | } 70 | 71 | .widget-baseclimate-{{id}} .secondary-icon.minus i { 72 | padding-top: 10px; 73 | padding-right: 30px; 74 | } 75 | -------------------------------------------------------------------------------- /docs/UPGRADE_FROM_3.x.rst: -------------------------------------------------------------------------------- 1 | Upgrading from 3.x 2 | ================== 3 | 4 | This documentation is for AppDaemon is 4.0.0 or later. If you are upgrading from a 3.x version, there have been some changes to the way AppDaemon is configured, and you will need to edit your config files and make some other changes. The changes are listed below: 5 | 6 | Note that not all changes will apply to everyone, some of them are in fairly obscure parts of AppDaemon that few if any people use, however, everyone will have to make some changes, so read carefully. 7 | 8 | - ``log`` section is deprecated in favor of a new and more versatile ``logs`` section. In AppDaemon 4.x, each log can be configured individually for filename, maximum size, etc. and in addition, it now supports custom formats and additional user logs. 9 | 10 | For more detail see the ``Log Configuration`` section in the Configuration section. 11 | 12 | - ``api_port`` is no longer supported by the ``appdaemon`` section, it has moved to the new ``http`` component, and is defined by the port number in the ``url`` parameter. API Paths to apps have not changed. The App API, Dashboards and new Admin interface all share a single port, configured in the `http` section. For further details, see ``Configuring the HTTP Component`` in the Configuration section. To turn on support for the App Api, you will need to include an ``api`` section in AppDaemon.yaml - see the ``Configuring the API`` section in the Configuration section/ 13 | 14 | - ``latitude``, ``longitude``, ``elevation`` and ``timezone`` are now mandatory and are specified in the ``appdaemon`` section of appdaemon.yaml. 15 | -------------------------------------------------------------------------------- /conf/example_dashboards/Modular/Secure.dash: -------------------------------------------------------------------------------- 1 | # 2 | # Main arguments, all optional 3 | # 4 | title: Security Panel 5 | widget_dimensions: [27, 29] 6 | widget_margins: [5, 5] 7 | columns: 32 8 | rows: 25 9 | widget_size: [4, 4] 10 | global_parameters: 11 | #use_comma: 1 12 | #precision: 2 13 | #use_hass_icon: 1 14 | 15 | downstairs_perimeter: 16 | widget_type: label 17 | title: Downstairs Perimeter 18 | 19 | motion: 20 | widget_type: label 21 | title: Motion 22 | 23 | upstairs_perimeter: 24 | widget_type: label 25 | title: Upstairs Perimeter 26 | 27 | basement_perimeter: 28 | widget_type: label 29 | title: Basement Perimeter 30 | 31 | garage_perimeter: 32 | widget_type: label 33 | title: Garage Perimeter 34 | 35 | garage: 36 | widget_type: cover 37 | title: Garage 38 | entity: cover.garage_door 39 | icon_on: fa-car 40 | icon_off: fa-car 41 | 42 | load_main_panel: 43 | widget_type: navigate 44 | title: Main Panel 45 | icon_inactive: fa-home 46 | dashboard: MainPanel 47 | 48 | layout: 49 | - motion(32x1) 50 | - binary_sensor.upstairs_sensor, binary_sensor.basement_sensor, binary_sensor.garage_sensor, binary_sensor.downstairs_sensor, binary_sensor.drive_sensor, binary_sensor.porch_multisensor_sensor, binary_sensor.side_multisensor_sensor 51 | - empty: 3 52 | - downstairs_perimeter(32x1) 53 | - lock.front_door_lock_locked 54 | - empty: 3 55 | - garage_perimeter(8x1), upstairs_perimeter(16x1), basement_perimeter(8x1) 56 | - garage, spacer(20x1), binary_sensor.basement_door_sensor 57 | - empty: 7 58 | - spacer(28x1), load_main_panel 59 | -------------------------------------------------------------------------------- /appdaemon/plugins/hass/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from enum import Enum, auto 3 | from typing import TYPE_CHECKING 4 | 5 | from appdaemon import utils 6 | 7 | if TYPE_CHECKING: 8 | from .hassplugin import HassPlugin 9 | 10 | 11 | class ServiceCallStatus(Enum): 12 | OK = auto() 13 | TIMEOUT =auto() 14 | TERMINATING = auto() 15 | 16 | 17 | def looped_coro(coro, sleep_time: int | float): 18 | """Repeatedly runs a coroutine, sleeping between runs""" 19 | 20 | @functools.wraps(coro) 21 | async def loop(self: "HassPlugin", *args, **kwargs): 22 | while not self.AD.stopping: 23 | try: 24 | await coro() 25 | except Exception: 26 | sleep_time_str = utils.format_timedelta(sleep_time) 27 | self.logger.error(f"Error running {coro.__name__} - retrying in {sleep_time_str}") 28 | finally: 29 | await self.AD.utility.sleep(sleep_time, timeout_ok=True) 30 | 31 | return loop 32 | 33 | 34 | def hass_check(func): 35 | """Essentially swallows the function call if the Home Assistant plugin isn't connected, in which case the function will return None. 36 | """ 37 | async def no_func(): 38 | pass 39 | 40 | @functools.wraps(func) 41 | def func_wrapper(self: "HassPlugin", *args, **kwargs): 42 | if not self.connect_event.is_set(): 43 | self.logger.warning("Attempt to call Home Assistant while disconnected: %s", func.__name__) 44 | return no_func() 45 | else: 46 | return func(self, *args, **kwargs) 47 | 48 | return func_wrapper 49 | -------------------------------------------------------------------------------- /conf/example_apps/ical.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import requests 3 | import ics 4 | import arrow 5 | from datetime import datetime, timedelta 6 | 7 | """ 8 | 9 | Load and publish iCal data, e.g a Google Calendar feed 10 | 11 | NB: This app need the "ics" Python module. 12 | 13 | Arguments: 14 | - event: Entity name when publishing event 15 | - interval: Update interval, in minutes 16 | - feed: Feed url 17 | - max_days: Maximum number of days to include 18 | - max_events: Maximum number of calendar events to include 19 | 20 | """ 21 | 22 | 23 | class Calendar(hass.Hass): 24 | def initialize(self): 25 | self.feed = self.args["feed"] 26 | self.entity = self.args["event"] 27 | self.max_events = int(self.args["max_events"]) 28 | self.max_days = int(self.args["max_days"]) 29 | interval = int(self.args["interval"]) 30 | 31 | inOneMinute = datetime.now() + timedelta(minutes=1) 32 | self.run_every(self.updateState, inOneMinute, interval * 60) 33 | 34 | def updateState(self, kwargs=None): 35 | self.log("loading data from ical feed") 36 | data = requests.get(self.feed).text 37 | ical = ics.Calendar(data) 38 | self.log("ical data loaded") 39 | 40 | now = arrow.now() 41 | future = arrow.now().replace(days=self.max_days) 42 | 43 | events = [ 44 | {"name": e.name, "location": e.location, "begin": e.begin.isoformat(), "end": e.end.isoformat()} 45 | for e in ical.events 46 | if now < e.begin < future 47 | ][: self.max_events] 48 | 49 | self.set_app_state(self.entity, {"state": "", "attributes": events}) 50 | -------------------------------------------------------------------------------- /conf/example_apps/grandfather.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | import datetime 3 | 4 | # 5 | # Grandfather clock APP inspired by @areeshmu: 6 | # 7 | # https://community.home-assistant.io/t/grand-father-clock-chime/9465 8 | # 9 | # Implements a Grandfather clock that sounds chimes through a media player 10 | # 11 | # Args: 12 | # 13 | # player - media player to use for chimes 14 | # volume - volume to use for chimes 15 | # media - path to media files (see link above for download) 16 | # 17 | # Version 1.0: 18 | # Initial Version 19 | 20 | 21 | class Grandfather(hass.Hass): 22 | def initialize(self): 23 | time = datetime.time(0, 0, 0) 24 | self.run_hourly(self.check_chime, time) 25 | 26 | def check_chime(self, kwargs): 27 | chime = True 28 | # self.verbose_log("Checking Chime") 29 | if "mute_if_home" in self.args and self.get_state(self.args["mute_if_home"]) == "home": 30 | # self.verbose_log("Wendy is home") 31 | chime = False 32 | if self.noone_home(): 33 | # self.verbose_log ("No one is home") 34 | chime = False 35 | if not self.now_is_between(self.args["start_time"], self.args["end_time"]): 36 | # self.verbose_log("It's early or late") 37 | chime = False 38 | if chime: 39 | # self.verbose_log("Chiming") 40 | self.chime() 41 | 42 | def chime(self): 43 | hour = self.time().hour 44 | if hour > 12: 45 | hour = hour - 12 46 | media = "{0}/GrandFatherChime{1:0=2d}.wav".format(self.args["media"], hour) 47 | sound = self.get_app("Sound") 48 | sound.play(media, "audio/wav", self.args["volume"], 65) 49 | -------------------------------------------------------------------------------- /docs/REST_STREAM_API.rst: -------------------------------------------------------------------------------- 1 | Stream Docs (High Level): 2 | ========================= 3 | 4 | The provides a framework for JSON based requests and responses in stream. 5 | 6 | Requests are JSON arrays that contain a "request_type" key. 7 | Optionally "request_id" can be sent in order to track the response. 8 | The "data" key is used for data to be sent to the request. 9 | 10 | Responses come with a "response_type" key equal to the request_type. If "request_id" was sent, "response_id" will be provided. "response_success" will be true or false. If false, "response_error" will be provided as well as "request" which contains the original request. If the response is successful, data MAY be provided if the request returned data. If a request_id is sent, a response message will always be generated, even if there is no data. 11 | 12 | The following requests types are established: 13 | 14 | hello 15 | ----- 16 | 17 | Requires a client_name key 18 | Accepts a password key with a plain text password 19 | Accepts a cookie key with a browser authorization cookie 20 | Will allow no password if none is set in AD config. 21 | 22 | listen_state 23 | ------------ 24 | 25 | Requires a namespace key. * wildcard supported at the end of the string 26 | Requires an entity_id key. * wildcard supported at the end of the string 27 | 28 | listen_event 29 | ------------ 30 | 31 | Requires a namespace key. * wildcard supported at the end of the string. 32 | Requires an event key. * wildcard supported at the end of the string. 33 | 34 | get_state 35 | --------- 36 | 37 | Requires no parameters. Returns all states in AppDaemon 38 | 39 | call_service: 40 | ------------- 41 | 42 | requires namespace, domain, service 43 | optionally, data can be provided for service data. 44 | -------------------------------------------------------------------------------- /tests/functional/test_startup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | from appdaemon.appdaemon import AppDaemon 5 | 6 | logger = logging.getLogger("AppDaemon._test") 7 | 8 | 9 | @pytest.mark.ci 10 | @pytest.mark.functional 11 | @pytest.mark.parametrize("app_name", ["hello_world", "another_app"]) 12 | @pytest.mark.asyncio(loop_scope="session") 13 | async def test_hello_world(ad: AppDaemon, caplog: pytest.LogCaptureFixture, app_name: str) -> None: 14 | """Run one of the hello world apps and ensure that the startup text is in the logs.""" 15 | 16 | ad.app_dir = ad.config_dir / "apps/hello_world" 17 | assert ad.app_dir.exists(), "App directory does not exist" 18 | logger.info("Test started") 19 | with caplog.at_level(logging.DEBUG, logger="AppDaemon"): 20 | async with ad.app_management.app_run_context(app_name): 21 | await ad.utility.app_update_event.wait() 22 | logger.info("Test completed") 23 | 24 | assert "Hello from AppDaemon" in caplog.text 25 | assert "You are now ready to run Apps!" in caplog.text 26 | 27 | 28 | @pytest.mark.ci 29 | @pytest.mark.functional 30 | @pytest.mark.asyncio(loop_scope="session") 31 | async def test_no_plugins(ad_obj: AppDaemon, caplog: pytest.LogCaptureFixture) -> None: 32 | """Ensure that apps start correctly when there are no plugins configured.""" 33 | ad_obj.config.plugins = {} 34 | ad_obj.app_dir = ad_obj.config_dir / "apps/hello_world" 35 | 36 | ad_obj.start() 37 | with caplog.at_level(logging.INFO, logger="AppDaemon"): 38 | async with ad_obj.app_management.app_run_context("hello_world"): 39 | await ad_obj.utility.app_update_event.wait() 40 | 41 | await ad_obj.stop() 42 | assert not any(r.levelname == "ERROR" for r in caplog.records) 43 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections.abc import Generator, Iterable 3 | from datetime import datetime, timedelta 4 | from itertools import pairwise 5 | from logging import LogRecord 6 | 7 | import pytest 8 | 9 | logger = logging.getLogger("AppDaemon._test") 10 | 11 | 12 | def filter_caplog(caplog: pytest.LogCaptureFixture, search_str: str) -> Generator[LogRecord]: 13 | """Count the number of log records at a specific level.""" 14 | for record in caplog.records: 15 | if search_str in record.msg: 16 | yield record 17 | 18 | 19 | def time_diffs(records: Iterable[LogRecord]) -> Generator[timedelta]: 20 | """Calculate time differences between consecutive log records.""" 21 | times = (datetime.strptime(r.asctime, "%Y-%m-%d %H:%M:%S.%f") for r in records) 22 | yield from (t2 - t1 for t1, t2 in pairwise(times)) 23 | 24 | 25 | def assert_timedelta( 26 | records: Iterable[LogRecord], 27 | expected: timedelta, 28 | buffer: timedelta = timedelta(microseconds=10000), 29 | ) -> None: 30 | """Assert that all time differences between consecutive log records match the expected timedelta.""" 31 | 32 | lines = ((r.msg, r.asctime) for r in records) 33 | zipped = zip(pairwise(records), time_diffs(records)) 34 | for lines, diff in zipped: 35 | try: 36 | assert (diff - expected) <= buffer, "Too much discrepancy in time difference" 37 | except AssertionError: 38 | logger.error(f"Wrong amount of time between log entries: {diff}") 39 | logger.error(f" {lines[0].asctime} {lines[0].msg} at ") 40 | logger.error(f" {lines[1].asctime} {lines[1].msg} at ") 41 | raise 42 | 43 | # assert all((diff - expected) <= buffer for diff in time_diffs(records)) 44 | -------------------------------------------------------------------------------- /appdaemon/widgets/baseentitypicture/baseentitypicture.js: -------------------------------------------------------------------------------- 1 | function baseentitypicture(widget_id, url, skin, parameters) 2 | { 3 | self = this 4 | 5 | // Initialization 6 | 7 | self.parameters = parameters; 8 | 9 | var callbacks = [] 10 | 11 | self.OnStateAvailable = OnStateAvailable; 12 | self.OnStateUpdate = OnStateUpdate; 13 | 14 | var monitored_entities = 15 | [ 16 | {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} 17 | ]; 18 | 19 | if ("base_url" in parameters && parameters.base_url != "") { 20 | self.base_url = parameters.base_url; 21 | }else{ 22 | self.base_url = ""; 23 | } 24 | 25 | // Call the parent constructor to get things moving 26 | WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); 27 | 28 | // Function Definitions 29 | 30 | function OnStateAvailable(self, state) 31 | { 32 | set_view(self, state) 33 | } 34 | 35 | // The OnStateUpdate function will be called when the specific entity 36 | // receives a state update - its new values will be available 37 | // in self.state[] and returned in the state parameter 38 | 39 | function OnStateUpdate(self, state) 40 | { 41 | set_view(self, state) 42 | } 43 | 44 | function set_view(self, state) 45 | { 46 | if("entity_picture" in state.attributes){ 47 | self.set_field(self, "img_inernal_src", self.base_url + state.attributes["entity_picture"]); 48 | self.set_field(self, "img_internal_style", ""); 49 | }else{ 50 | self.set_field(self, "img_inernal_src", ""); 51 | self.set_field(self, "img_internal_style", "display: none;"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /conf/example_apps/sequence.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | 4 | """ 5 | App to fire a sequence of events when a particular state is met 6 | Args: 7 | input: switch or device to monitor to fire the sequence 8 | state: new state that fires the sequence 9 | sequence a list of sequence entries: 10 | - entity: entity to call service on 11 | service: name of the service 12 | delay: delay from the event firing after which this entry will activate 13 | 14 | e.g. 15 | 16 | input: input_boolean.studio 17 | state: "off" 18 | sequence: 19 | - entity: switch.basement_speakers_switch 20 | service: switch/turn_off 21 | delay: 0 22 | - entity: switch.basement_desk_switch 23 | service: switch/turn_off 24 | delay: 5 25 | 26 | Release Notes 27 | Version 1.0: 28 | Initial Version 29 | """ 30 | 31 | 32 | class Sequence(hass.Hass): 33 | def initialize(self): 34 | if "input" in self.args and "state" in self.args: 35 | self.listen_state(self.state_change, self.args["input"], new=self.args["state"]) 36 | 37 | def state_change(self, entity, attribute, old, new, kwargs): 38 | # self.verbose_log("{} turned {}".format(entity, new)) 39 | if "sequence" in self.args: 40 | for entry in self.args["sequence"]: 41 | self.run_in( 42 | self.action, 43 | entry["delay"], 44 | device=entry["entity"], 45 | service=entry["service"], 46 | ) 47 | 48 | def action(self, kwargs): 49 | self.log("Calling {} on {}".format(kwargs["device"], kwargs["service"])) 50 | self.call_service(kwargs["service"], entity_id=kwargs["device"]) 51 | -------------------------------------------------------------------------------- /appdaemon/widgets/basemedia/basemedia.css: -------------------------------------------------------------------------------- 1 | .widget-basemedia-{{id}} .title { 2 | position: absolute; 3 | top: 5px; 4 | width: 100%; 5 | } 6 | 7 | .widget-basemedia-{{id}} .artist { 8 | position: absolute; 9 | top: 30px; 10 | width: 100%; 11 | } 12 | 13 | .widget-basemedia-{{id}} .album { 14 | position: absolute; 15 | top: 55px; 16 | width: 100%; 17 | } 18 | 19 | .widget-basemedia-{{id}} .media_title { 20 | position: absolute; 21 | top: 80px; 22 | width: 100%; 23 | } 24 | 25 | .widget-basemedia-{{id}} .state_text { 26 | position: absolute; 27 | top: 38px; 28 | width: 100%; 29 | } 30 | 31 | .widget-basemedia-{{id}} .previous { 32 | position: absolute; 33 | top: 130px; 34 | left: 25px; 35 | z-index: 10; 36 | } 37 | 38 | .widget-basemedia-{{id}} .play { 39 | position: absolute; 40 | top: 130px; 41 | width: 100%; 42 | } 43 | 44 | .widget-basemedia-{{id}} .next { 45 | position: absolute; 46 | top: 130px; 47 | right: 25px; 48 | z-index: 10; 49 | } 50 | 51 | 52 | .widget-basemedia-{{id}} .level { 53 | display: inline-block; 54 | } 55 | 56 | .widget-basemedia-{{id}} .unit { 57 | display: inline-block; 58 | } 59 | 60 | .widget-basemedia-{{id}} .levelunit { 61 | position: absolute; 62 | bottom: 5px; 63 | width: 100%; 64 | } 65 | 66 | .widget-basemedia-{{id}} .secondary-icon { 67 | position: absolute; 68 | bottom: 0px; 69 | font-size: 20px; 70 | width: 32px; 71 | color: white; 72 | } 73 | 74 | .widget-basemedia-{{id}} .secondary-icon.plus { 75 | right: 24px; 76 | } 77 | 78 | .widget-basemedia-{{id}} .secondary-icon.plus i { 79 | padding-top: 10px; 80 | padding-left: 10px; 81 | } 82 | 83 | .widget-basemedia-{{id}} .secondary-icon.minus { 84 | left: 28px; 85 | } 86 | 87 | .widget-basemedia-{{id}} .secondary-icon.minus i { 88 | padding-top: 10px; 89 | padding-right: 30px; 90 | } 91 | -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Contributor License Agreement 2 | 3 | ``` 4 | By making a contribution to this project, I certify that: 5 | 6 | (a) The contribution was created in whole or in part by me and I 7 | have the right to submit it under the Apache 2.0 license; or 8 | 9 | (b) The contribution is based upon previous work that, to the best 10 | of my knowledge, is covered under an appropriate open source 11 | license and I have the right under that license to submit that 12 | work with modifications, whether created in whole or in part 13 | by me, under the Apache 2.0 license; or 14 | 15 | (c) The contribution was provided directly to me by some other 16 | person who certified (a), (b) or (c) and I have not modified 17 | it. 18 | 19 | (d) I understand and agree that this project and the contribution 20 | are public and that a record of the contribution (including all 21 | personal information I submit with it) is maintained indefinitely 22 | and may be redistributed consistent with this project or the open 23 | source license(s) involved. 24 | ``` 25 | 26 | ## Attribution 27 | 28 | The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the Apache 2.0 license 29 | and not mention sign-off. 30 | 31 | ## Signing 32 | 33 | To sign this CLA you must first submit a pull request to a repository under the Home Assistant organization. 34 | 35 | ## Adoption 36 | 37 | This Contributor License Agreement (CLA) was first announced on January 21st, 2017 in [this][cla-blog] blog post and adopted January 28th, 2017. 38 | 39 | [cla-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ 40 | -------------------------------------------------------------------------------- /appdaemon/models/notification/iOS.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from pydantic import BaseModel, Field, field_validator 3 | 4 | from .base import NotificationData, Payload, NotifyAction, Action 5 | 6 | 7 | class iOSSound(BaseModel, extra="forbid"): 8 | name: str 9 | critical: bool | None = None 10 | volume: int | None = None 11 | 12 | 13 | class iOSPush(BaseModel, extra="forbid"): 14 | badge: int | None = None 15 | sound: iOSSound | None = None 16 | interruption_level: Literal["passive", "active", "time-sensitive", "critical"] | None = Field(default=None, serialization_alias="interruption-level") 17 | presentation_options: list[Literal["alert", "badge", "sound"]] | None = None 18 | 19 | 20 | class iOSAction(Action, extra="forbid"): 21 | activation_mode: Literal["foreground", "background"] | None = Field(default=None, serialization_alias="activationMode") 22 | authentication_required: bool | None = Field(default=None, serialization_alias="authenticationRequired") 23 | destructive: bool | None = None 24 | behavior: str | None = None 25 | text_input_button_title: str | None = Field(default=None, serialization_alias="textInputButtonTitle") 26 | text_input_placeholder: str | None = Field(default=None, serialization_alias="textInputPlaceholder") 27 | icon: str | None = None 28 | 29 | @field_validator("icon", mode="before") 30 | @classmethod 31 | def validate_icon(cls, v: str): 32 | assert v.startswith("sfsymbols:") 33 | return v 34 | 35 | 36 | class iOSPayload(Payload, extra="forbid"): 37 | url: str | None = None 38 | subject: str | None = None 39 | push: iOSPush | None = None 40 | actions: list[iOSAction] | None = None 41 | 42 | 43 | class iOSData(NotificationData, extra="forbid"): 44 | data: iOSPayload | None = None 45 | 46 | 47 | class iOSAction(NotifyAction): 48 | data: iOSData 49 | -------------------------------------------------------------------------------- /conf/example_apps/minimote.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | # 4 | # App to respond to buttons on an Aeotec Minimote 5 | # 6 | # Args: 7 | # 8 | # Minimote can send up to 8 scenes. Odd numbered scenes are short presses of the buttons, even are long presses 9 | # 10 | # Args: 11 | # 12 | # device - name of the device. This will be the ZWave name without an entity type, e.g. minimote_31 13 | # scene__on - name of the entity to turn on when scene is activated 14 | # scene__off - name of the entity to turn off when scene is activated. If the entity is a scene it will be turned on. 15 | # scene__toggle - name of the entity to toggle when scene is activated 16 | # 17 | # Each scene can have up to one of each type of action, or no actions - e.g. you can turn on one light and turn off another light for a particular scene if desired 18 | # 19 | # Release Notes 20 | # 21 | # Version 1.0: 22 | # Initial Version 23 | 24 | 25 | class MiniMote(hass.Hass): 26 | def initialize(self): 27 | self.listen_event(self.zwave_event, "zwave.scene_activated", entity_id=self.args["device"]) 28 | 29 | def zwave_event(self, event_name, data, kwargs): 30 | # self.verbose_log("Event: {}, data = {}, args = {}".format(event_name, data, kwargs)) 31 | scene = data["scene_id"] 32 | on = "scene_{}_on".format(scene) 33 | off = "scene_{}_off".format(scene) 34 | toggle = "scene_{}_toggle".format(scene) 35 | 36 | if on in self.args: 37 | self.log("Turning {} on".format(self.args[on])) 38 | self.turn_on(self.args[on]) 39 | 40 | if off in self.args: 41 | self.log("Turning {} off".format(self.args[off])) 42 | self.turn_off(self.args[off]) 43 | 44 | if toggle in self.args: 45 | self.log("Toggling {}".format(self.args[toggle])) 46 | self.toggle(self.args[toggle]) 47 | -------------------------------------------------------------------------------- /tests/functional/test_production_mode.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import AsyncMock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | from appdaemon.appdaemon import AppDaemon 7 | 8 | 9 | @pytest_asyncio.fixture(scope="function") 10 | async def ad_production(ad_obj: AppDaemon): 11 | """AppDaemon fixture with production_mode enabled.""" 12 | ad_obj.config.production_mode = True 13 | ad_obj.app_dir = ad_obj.config_dir / "apps/hello_world" 14 | 15 | ad_obj.start() 16 | yield ad_obj 17 | await ad_obj.stop() 18 | 19 | 20 | @pytest.mark.ci 21 | @pytest.mark.functional 22 | @pytest.mark.asyncio(loop_scope="session") 23 | async def test_production_mode_loads_apps(ad_production: AppDaemon) -> None: 24 | """Test that apps load correctly when production_mode is enabled.""" 25 | # Wait for initialization to complete 26 | await ad_production.utility.app_update_event.wait() 27 | # Check that the app loaded 28 | assert "hello_world" in ad_production.app_management.objects 29 | 30 | 31 | @pytest.mark.ci 32 | @pytest.mark.functional 33 | @pytest.mark.asyncio(loop_scope="session") 34 | async def test_production_mode_no_reloading(ad_production: AppDaemon) -> None: 35 | """Test that production mode doesn't reload apps when files change.""" 36 | # Wait for initialization to complete 37 | await ad_production.utility.app_update_event.wait() 38 | 39 | # Mock check_app_updates to track calls from now on 40 | mock = AsyncMock(wraps=ad_production.app_management.check_app_updates) 41 | ad_production.app_management.check_app_updates = mock 42 | 43 | # Touch file and wait for utility loop 44 | ad_production.utility.app_update_event.clear() 45 | os.utime(ad_production.app_dir / "hello.py", None) 46 | await ad_production.utility.app_update_event.wait() 47 | 48 | assert not mock.called, "Should not reload in production mode" 49 | -------------------------------------------------------------------------------- /docs/COMMUNITY_TUTORIALS.rst: -------------------------------------------------------------------------------- 1 | Community Tutorials 2 | =================== 3 | 4 | Here is a list of other tutorials that have been created by AppDaemon users: 5 | 6 | - `AppDaemon For Beginners `__ 7 | - `AppDaemon Tutorial #1 Tracker-Notifier `__ 8 | - `AppDaemon Tutorial #2 Errorlog Notifications `__ 9 | - `AppDaemon Tutorial #3 Utility Functions `__ 10 | - `AppDaemon Tutorial #4 Libraries & Interactivity `__ 11 | - `Home Presence Appdaemon App `__ 12 | - `App #1: Doorbell notification `__ 13 | - `App #2: Smart Light `__ 14 | - `App #3: Smart Radiator `__ 15 | - `App #4: Boiler Alert `__ 16 | - `App #5: Smart Radiator (Generic) `__ 17 | - `App #6: Window Alert `__ 18 | - `App #7: Boiler Temperature Alert `__ 19 | - `App #8: Detect a particular sequence of events `__ 20 | 21 | Do you have other tutorials? Make a PR :) 22 | 23 | -- AppDaemon Team 24 | -------------------------------------------------------------------------------- /tests/functional/test_run_every.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import uuid 4 | from datetime import timedelta 5 | from functools import partial 6 | from itertools import product 7 | 8 | import pytest 9 | from appdaemon.utils import parse_timedelta 10 | 11 | from tests.conftest import AsyncTempTest 12 | 13 | from .utils import check_interval 14 | 15 | logger = logging.getLogger("AppDaemon._test") 16 | 17 | 18 | INTERVALS = ["00:0.35", 1, timedelta(seconds=0.87)] 19 | STARTS = ["now - 00:00.5", "now", "now + 00:0.5"] 20 | 21 | 22 | @pytest.mark.asyncio(loop_scope="session") 23 | @pytest.mark.parametrize(("start", "interval"), product(STARTS, INTERVALS)) 24 | async def test_run_every( 25 | run_app_for_time: AsyncTempTest, 26 | interval: str | int | float | timedelta, 27 | start: str, 28 | n: int = 2, 29 | ) -> None: 30 | interval = parse_timedelta(interval) 31 | run_time = (interval * n) + timedelta(seconds=0.01) 32 | register_delay = 0.1 33 | run_time += timedelta(seconds=register_delay) # Accounts for the delay in registering the callback 34 | if (parts := re.split(r"\s+[\+]\s+", start)) and len(parts) == 2: 35 | _, offset = parts 36 | run_time += parse_timedelta(offset) 37 | 38 | app_name = "scheduler_test_app" 39 | test_id = str(uuid.uuid4()) 40 | app_args = dict(start=start, interval=interval, msg=test_id, register_delay=register_delay) 41 | async with run_app_for_time(app_name, run_time=run_time.total_seconds(), **app_args) as (ad, caplog): 42 | check_interval_partial = partial(check_interval, caplog, f"kwargs: {{'msg': '{test_id}',") 43 | 44 | if start.startswith("now -"): 45 | check_interval_partial(n, interval) 46 | else: 47 | check_interval_partial(n + 1, interval) 48 | 49 | # diffs = utils.time_diffs(utils.filter_caplog(caplog, test_id)) 50 | # logger.debug(diffs) 51 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | sys.path.insert(0, os.path.abspath("..")) 16 | 17 | from appdaemon.version import __version__ # noqa: E402 18 | 19 | project = "AppDaemon" 20 | copyright = "2016-2023, Andrew Cockburn" 21 | author = "Andrew Cockburn" 22 | release = __version__ 23 | 24 | # -- General configuration --------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 26 | 27 | extensions = [ 28 | "sphinx_copybutton", 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.napoleon", 31 | "sphinx.ext.intersphinx", 32 | "sphinx_tabs.tabs", 33 | "myst_parser" 34 | ] 35 | 36 | copybutton_exclude = '.linenos, .gp' 37 | 38 | templates_path = ["_templates"] 39 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 43 | 44 | html_theme = "sphinx_rtd_theme" 45 | html_static_path = ["css"] 46 | 47 | # -- Intersphinx configuration ----------------------------------------------- 48 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html 49 | 50 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 51 | -------------------------------------------------------------------------------- /appdaemon/widgets/media_player.yaml: -------------------------------------------------------------------------------- 1 | widget_type: basemedia 2 | entity: "{{entity}}" 3 | post_service_next: 4 | service: media_player/media_next_track 5 | entity_id: "{{entity}}" 6 | post_service_previous: 7 | service: media_player/media_previous_track 8 | entity_id: "{{entity}}" 9 | post_service_play_pause: 10 | service: media_player/media_play_pause 11 | entity_id: "{{entity}}" 12 | post_service_pause: 13 | service: media_player/media_pause 14 | entity_id: "{{entity}}" 15 | post_service_stop: 16 | service: media_player/media_stop 17 | entity_id: "{{entity}}" 18 | post_service_level: 19 | service: media_player/volume_set 20 | entity_id: "{{entity}}" 21 | fields: 22 | title: "{{title}}" 23 | artist: "" 24 | media_title: "" 25 | album: "" 26 | play_icon_style: "" 27 | pause_icon_style: "" 28 | previous_icon_style: "" 29 | next_icon_style: "" 30 | state_text: "" 31 | level: "" 32 | icons: 33 | play_icon: $media_player_icon_play 34 | pause_icon: $media_player_icon_pause 35 | static_icons: 36 | previous_icon: $media_player_icon_previous 37 | next_icon: $media_player_icon_next 38 | icon_up: $media_player_icon_up 39 | icon_down: $media_player_icon_down 40 | static_css: 41 | previous_icon_style: $media_player_icon_style_previous 42 | next_icon_style: $media_player_icon_style_next 43 | title_style: $media_player_title_style 44 | artist_style: $media_player_artist_style 45 | album_style: $media_player_album_style 46 | media_title_style: $media_player_media_title_style 47 | state_text_style: $media_player_state_text_style 48 | level_style: $media_player_level_style 49 | level_up_style: $media_player_level_up_style 50 | level_down_style: $media_player_level_down_style 51 | widget_style: $media_player_widget_style 52 | units_style: $media_player_units_style 53 | css: 54 | icon_style_active: $media_player_icon_style_active 55 | icon_style_inactive: $media_player_icon_style_inactive 56 | -------------------------------------------------------------------------------- /appdaemon/widgets/weather.yaml: -------------------------------------------------------------------------------- 1 | widget_type: baseweather 2 | fields: 3 | title: "" 4 | show_forecast: 0 5 | prefer_icons: 0 6 | unit: "" 7 | wind_unit: "" 8 | pressure_unit: "" 9 | rain_unit: "" 10 | temperature: "" 11 | humidity: "" 12 | precip_probability: "" 13 | precip_intensity: "" 14 | precip_type: "" 15 | wind_speed: "" 16 | pressure: "" 17 | wind_bearing: "" 18 | apparent_temperature: "" 19 | icon: "" 20 | bearing_icon: "mdi-rotate-0" 21 | precip_type_icon: "mdi-umbrella" 22 | forecast_title: "" 23 | forecast_temperature_min: "" 24 | forecast_temperature_max: "" 25 | forecast_icon: "" 26 | forecast_precip_probability: "" 27 | forecast_precip_type: "" 28 | forecast_precip_type_icon: "mdi-umbrella" 29 | entities: 30 | icon: sensor.dark_sky_icon 31 | temperature: sensor.dark_sky_temperature 32 | apparent_temperature: sensor.dark_sky_apparent_temperature 33 | humidity: sensor.dark_sky_humidity 34 | precip_probability: sensor.dark_sky_precip_probability 35 | precip_intensity: sensor.dark_sky_precip_intensity 36 | precip_type: sensor.dark_sky_precip 37 | pressure: sensor.dark_sky_pressure 38 | wind_speed: sensor.dark_sky_wind_speed 39 | wind_bearing: sensor.dark_sky_wind_bearing 40 | forecast_icon: sensor.dark_sky_icon_1d 41 | forecast_temperature_min: sensor.dark_sky_daily_low_temperature_1d 42 | forecast_temperature_max: sensor.dark_sky_daily_high_temperature_1d 43 | forecast_precip_probability: sensor.dark_sky_precip_probability_1d 44 | forecast_precip_type: sensor.dark_sky_precip_1d 45 | css: [] 46 | static_css: 47 | title_style: $weather_sub_style 48 | unit_style: $weather_unit_style 49 | main_style: $weather_main_style 50 | sub_style: $weather_sub_style 51 | sub_unit_style: $weather_sub_style 52 | widget_style: $weather_widget_style 53 | icons: 54 | snow: mdi-snowflake 55 | rain: mdi-umbrella 56 | sleet: mdi-weather-snowy-rainy 57 | unknown: mdi-umbrella 58 | static_icons: [] 59 | -------------------------------------------------------------------------------- /appdaemon/assets/templates/error.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Configuration Error 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
AppDaemon Administration
30 |

Configuration Error - please configure the admin interface or dashboard component

31 |
32 |
33 |
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
34 | 35 | 36 | -------------------------------------------------------------------------------- /appdaemon/assets/templates/logon.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | AppDaemon Logon 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
AppDaemon Logon
31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /appdaemon/futures.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections import defaultdict 3 | from typing import TYPE_CHECKING 4 | from concurrent.futures import Future 5 | 6 | if TYPE_CHECKING: 7 | from appdaemon.appdaemon import AppDaemon 8 | 9 | 10 | class Futures: 11 | """Subsystem container for managing :class:`~asyncio.Future` objects 12 | """ 13 | 14 | AD: "AppDaemon" 15 | """Reference to the top-level AppDaemon container object""" 16 | futures: dict[str , list[asyncio.Future]] 17 | """Dictionary of futures registered by app name""" 18 | 19 | def __init__(self, ad: "AppDaemon"): 20 | self.AD = ad 21 | self.logger = self.AD.logging.get_child("_futures") 22 | self.futures = defaultdict(list) 23 | 24 | def add_future(self, app_name: str, future: asyncio.Future | Future): 25 | """Add a future to the registry and a callback that removes itself from the registry after it finishes.""" 26 | self.futures[app_name].append(future) 27 | def safe_remove(f): 28 | try: 29 | self.futures[app_name].remove(f) 30 | except ValueError: 31 | pass 32 | future.add_done_callback(safe_remove) 33 | 34 | match future: 35 | case asyncio.Task() as task: 36 | self.logger.debug(f"Registered a task for {app_name}: {task.get_name()}") 37 | case _: 38 | self.logger.debug(f"Registered a future for {app_name}: {future}") 39 | 40 | def cancel_future(self, future: asyncio.Future): 41 | if not future.done() and not future.cancelled(): 42 | if isinstance(future, asyncio.Task): 43 | self.logger.debug(f"Cancelling task {future.get_name()}") 44 | else: 45 | self.logger.debug(f"Cancelling future {future}") 46 | future.cancel() 47 | 48 | def cancel_futures(self, app_name: str): 49 | for f in self.futures.pop(app_name, []): 50 | self.cancel_future(f) 51 | -------------------------------------------------------------------------------- /conf/example_apps/motion_lights.py: -------------------------------------------------------------------------------- 1 | import hassapi as hass 2 | 3 | # 4 | # App to turn lights on when motion detected then off again after a delay 5 | # 6 | # Use with constraints to activate only for the hours of darkness 7 | # 8 | # Args: 9 | # 10 | # sensor: binary sensor to use as trigger 11 | # entity_on : entity to turn on when detecting motion, can be a light, script, scene or anything else that can be turned on 12 | # entity_off : entity to turn off when detecting motion, can be a light, script or anything else that can be turned off. Can also be a scene which will be turned on 13 | # delay: amount of time after turning on to turn off again. If not specified defaults to 60 seconds. 14 | # 15 | # Release Notes 16 | # 17 | # Version 1.1: 18 | # Add ability for other apps to cancel the timer 19 | # 20 | # Version 1.0: 21 | # Initial Version 22 | 23 | 24 | class MotionLights(hass.Hass): 25 | def initialize(self): 26 | self.handle = None 27 | 28 | # Check some Params 29 | 30 | # Subscribe to sensors 31 | if "sensor" in self.args: 32 | self.listen_state(self.motion, self.args["sensor"]) 33 | else: 34 | self.log("No sensor specified, doing nothing") 35 | 36 | def motion(self, entity, attribute, old, new, kwargs): 37 | if new == "on": 38 | if "entity_on" in self.args: 39 | self.log("Motion detected: turning {} on".format(self.args["entity_on"])) 40 | self.turn_on(self.args["entity_on"]) 41 | if "delay" in self.args: 42 | delay = self.args["delay"] 43 | else: 44 | delay = 60 45 | self.cancel_timer(self.handle) 46 | self.handle = self.run_in(self.light_off, delay) 47 | 48 | def light_off(self, kwargs): 49 | if "entity_off" in self.args: 50 | self.log("Turning {} off".format(self.args["entity_off"])) 51 | self.turn_off(self.args["entity_off"]) 52 | 53 | def cancel(self): 54 | self.cancel_timer(self.handle) 55 | -------------------------------------------------------------------------------- /appdaemon/models/config/misc.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | from pathlib import Path 4 | from typing import Any, Literal 5 | 6 | from pydantic import BaseModel, Field, model_validator 7 | 8 | from appdaemon.utils import ADWritebackType 9 | 10 | from .common import ParsedTimedelta 11 | 12 | LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 13 | 14 | 15 | class AppDaemonCLIKwargs(BaseModel): 16 | config: Path 17 | configfile: Path 18 | moduledebug: dict[str, LEVELS] = Field(default_factory=dict) 19 | debug: LEVELS | None = None 20 | timewarp: float | None = None 21 | starttime: datetime | None = None 22 | endtime: datetime | None = None 23 | profiledash: bool = False 24 | write_toml: bool = False 25 | pidfile: Path | None = None 26 | 27 | def print(self): 28 | print(json.dumps(self.model_dump(mode="json", exclude_defaults=True), indent=4)) 29 | 30 | 31 | class FilterConfig(BaseModel): 32 | command_line: str 33 | input_ext: str 34 | output_ext: str 35 | 36 | 37 | class NamespaceConfig(BaseModel): 38 | writeback: ADWritebackType | None = None 39 | persist: bool = Field(default=False, alias="persistent") 40 | save_interval: ParsedTimedelta = Field(default=timedelta(seconds=1)) 41 | 42 | @model_validator(mode="before") 43 | @classmethod 44 | def validate_persistence(cls, values: Any): 45 | """Sets persistence to True if writeback is set to safe or hybrid.""" 46 | match values: 47 | case {"writeback": wb} if wb is not None: 48 | values["persist"] = True 49 | case _ if getattr(values, "writeback", None) is not None: 50 | values.persist = True 51 | return values 52 | 53 | @model_validator(mode="after") 54 | def validate_writeback(self): 55 | """Makes the writeback safe by default if persist is set to True.""" 56 | if self.persist and self.writeback is None: 57 | self.writeback = ADWritebackType.safe 58 | return self 59 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | # First mark the issue as `stale` according to `days-before-stale`, then close it if no activity for `days-before-close` 2 | name: 'Close stale issues and PRs' 3 | on: 4 | schedule: 5 | # run at 00:30 on Sunday UTC 6 | - cron: '30 0 * * 0' 7 | # To manually trigger from Github UI and debug this workflow 8 | workflow_dispatch: 9 | 10 | permissions: 11 | # To label and close issues 12 | issues: write 13 | # To label and close PR 14 | pull-requests: write 15 | 16 | # For debug purposes 17 | env: 18 | DRY_RUN: "false" 19 | 20 | jobs: 21 | # Consider ONLY issue with label `question` OR `wait-response` 22 | stale_labeled: 23 | name: question, wait-response 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/stale@v10 27 | with: 28 | stale-issue-message: 'This issue is stale because it has been open for 45 days with no activity. Remove stale label or comment or this will be closed in 15 days.' 29 | close-issue-message: 'Please feel free re-open this issue if you have new information available' 30 | days-before-stale: 45 31 | days-before-close: 15 32 | any-of-labels: "question,docs_needed" 33 | stale-issue-label: "stale" 34 | debug-only: "${{ env.DRY_RUN }}" 35 | 36 | # Consider ONLY issues that DO NOT have any of the labels defined below 37 | stale_unlabeled: 38 | name: without label 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/stale@v10 42 | with: 43 | stale-issue-message: 'This issue is stale because it has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 15 days.' 44 | close-issue-message: 'Please feel free re-open this issue if you have new information available' 45 | days-before-stale: 180 46 | days-before-close: 15 47 | exempt-issue-labels: "bug,enhancement,documentation,question,docs_needed,work_in_progress" 48 | stale-issue-label: "stale" 49 | debug-only: "${{ env.DRY_RUN }}" 50 | --------------------------------------------------------------------------------