├── 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 |
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 |
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 |
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 |
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 |
7 |
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 |
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 |
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 |
30 |
Configuration Error - please configure the admin interface or dashboard component
31 |
32 |
33 |
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 |
31 |
32 |
33 |
34 |
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 |
--------------------------------------------------------------------------------