├── hacs.json ├── .github ├── workflows │ └── validate.yaml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── README.md └── todoist-card.js /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todoist Card", 3 | "content_in_root": true, 4 | "filename": "todoist-card.js", 5 | "render_readme": true 6 | } -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "plugin" 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: grinstantin 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help me improve this project 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: grinstantin 7 | 8 | --- 9 | 10 | **Before submitting a bug** 11 | - [ ] I updated to the latest card version available 12 | - [ ] I cleared the cache of my browser 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Versions:** 31 | - Card: [e.g. 1.0.5] 32 | - HomeAssistant: [e.g. 2021.3.4] 33 | - Browser: [e.g. Google Chrome 89.0.4389.114] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Konstantin Grinkevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todoist Card 2 | 3 | [](https://github.com/hacs/integration) 4 |  5 |  6 | 7 | Todoist card for [Home Assistant](https://www.home-assistant.io) Lovelace UI. This card displays items from selected Todoist project. 8 | 9 |  10 | 11 | ## Installing 12 | 13 | ### HACS 14 | 15 | This card is available in [HACS](https://hacs.xyz) (Home Assistant Community Store). 16 | 17 | Just search for `Todoist Card` in HACS `Frontend` tab. 18 | 19 | ### Manual 20 | 21 | 1. Download `todoist-card.js` file from the [latest release](https://github.com/grinstantin/todoist-card/releases/latest). 22 | 2. Put `todoist-card.js` file into your `config/www` folder. 23 | 3. Add a reference to `todoist-card.js` in Lovelace. There's two way to do that: 24 | 1. **Using UI:** _Configuration_ → _Lovelace Dashboards_ → _Resources_ → Click Plus button → Set _Url_ as `/local/todoist-card.js` → Set _Resource type_ as `JavaScript Module`. 25 | 2. **Using YAML:** Add the following code to `lovelace` section. 26 | ```yaml 27 | resources: 28 | - url: /local/todoist-card.js 29 | type: module 30 | ``` 31 | 4. Add `custom:todoist-card` to Lovelace UI as any other card (using either editor or YAML configuration). 32 | 33 | ## Using the card 34 | 35 | This card can be configured using Lovelace UI editor. 36 | 37 | 1. Add the following code to `configuration.yaml`: 38 | ```yaml 39 | sensor: 40 | - platform: rest 41 | name: To-do List 42 | method: GET 43 | resource: 'https://api.todoist.com/sync/v9/projects/get_data' 44 | params: 45 | project_id: TODOIST_PROJECT_ID 46 | headers: 47 | Authorization: !secret todoist_api_token 48 | value_template: '{{ value_json[''project''][''id''] }}' 49 | json_attributes: 50 | - project 51 | - items 52 | scan_interval: 30 53 | 54 | rest_command: 55 | todoist: 56 | method: post 57 | url: 'https://api.todoist.com/sync/v9/{{ url }}' 58 | payload: '{{ payload }}' 59 | headers: 60 | Authorization: !secret todoist_api_token 61 | content_type: 'application/x-www-form-urlencoded' 62 | ``` 63 | 2. ... and to `secrets.yaml`: 64 | ```yaml 65 | todoist_api_token: 'Bearer TODOIST_API_TOKEN' 66 | ``` 67 | 3. Replace `TODOIST_API_TOKEN` with your [token](https://app.todoist.com/app/settings/integrations/developer) 68 | 69 | > Important note! Replace only the `TODOIST_API_TOKEN` and keep the 'Bearer ' part unchanged. 70 | 71 | and `TODOIST_PROJECT_ID` with ID of your selected Todoist project. 72 | 73 | > `TODOIST_PROJECT_ID` contains only numbers. You can get it from project URL, which usually looks like this: 74 | `https://todoist.com/app/project/TODOIST_PROJECT_ID` 75 | 4. Reload configs or restart Home Assistant. 76 | 5. In Lovelace UI, click 3 dots in top left corner. 77 | 6. Click _Edit Dashboard_. 78 | 7. Click _Add Card_ button in the bottom right corner to add a new card. 79 | 8. Find _Custom: Todoist Card_ in the list. 80 | 9. Choose `entity`. 81 | 10. Now you should see the preview of the card! 82 | 83 | Typical example of using this card in YAML config would look like this: 84 | 85 | ```yaml 86 | type: 'custom:todoist-card' 87 | entity: sensor.to_do_list 88 | show_header: true 89 | show_completed: 5 90 | show_item_add: true 91 | use_quick_add: false 92 | show_item_close: true 93 | show_item_delete: true 94 | only_today_overdue: false 95 | ``` 96 | 97 | Here is what every option means: 98 | 99 | | Name | Type | Default | Description | 100 | | -------------------- | :-------: | :----------: | -------------------------------------------------------------------------------------------------------------------------------- | 101 | | `type` | `string` | **required** | `custom:todoist-card` | 102 | | `entity` | `string` | **required** | An entity_id within the `sensor` domain. | 103 | | `show_completed` | `integer` | `5` | Number of completed tasks shown at the end of the list (0 to disable). | 104 | | `show_header` | `boolean` | `true` | Show friendly name of the selected `sensor` in the card header. | 105 | | `show_item_add` | `boolean` | `true` | Show text input element for adding new items to the list. | 106 | | `use_quick_add` | `boolean` | `false` | Use the [Quick Add](https://todoist.com/help/articles/task-quick-add) implementation, available in the official Todoist clients. | 107 | | `show_item_close` | `boolean` | `true` | Show `close/complete` and `uncomplete` buttons. | 108 | | `show_item_delete` | `boolean` | `true` | Show `delete` buttons. | 109 | | `only_today_overdue` | `boolean` | `false` | Only show tasks that are overdue or due today. | 110 | 111 | > Note that the completed tasks list is cleared when the page is refreshed. 112 | 113 | ## Actions 114 | 115 | - _Circle_ marks selected task as completed. 116 | - _Plus_ "uncompletes" selected task, adding it back to the list. 117 | - _Trash bin_ deletes selected task (gray one deletes it only from the list of completed items, not from Todoist archive). 118 | - _Input_ adds new item to the list after pressing `Enter`. 119 | 120 | ## License 121 | 122 | MIT © [Konstantin Grinkevich](https://github.com/grinstantin) 123 | -------------------------------------------------------------------------------- /todoist-card.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'https://unpkg.com/lit-element@2.4.0/lit-element.js?module'; 2 | 3 | class TodoistCardEditor extends LitElement { 4 | static get properties() { 5 | return { 6 | hass: Object, 7 | config: Object, 8 | }; 9 | } 10 | 11 | get _entity() { 12 | if (this.config) { 13 | return this.config.entity || ''; 14 | } 15 | 16 | return ''; 17 | } 18 | 19 | get _show_completed() { 20 | if (this.config) { 21 | return (this.config.show_completed !== undefined) ? this.config.show_completed : 5; 22 | } 23 | 24 | return 5; 25 | } 26 | 27 | get _show_header() { 28 | if (this.config) { 29 | return this.config.show_header || true; 30 | } 31 | 32 | return true; 33 | } 34 | 35 | get _show_item_add() { 36 | if (this.config) { 37 | return this.config.show_item_add || true; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | get _use_quick_add() { 44 | if (this.config) { 45 | return this.config.use_quick_add || false; 46 | } 47 | 48 | return false; 49 | } 50 | 51 | get _show_item_close() { 52 | if (this.config) { 53 | return this.config.show_item_close || true; 54 | } 55 | 56 | return true; 57 | } 58 | 59 | get _show_item_delete() { 60 | if (this.config) { 61 | return this.config.show_item_delete || true; 62 | } 63 | 64 | return true; 65 | } 66 | 67 | get _only_today_overdue() { 68 | if (this.config) { 69 | return this.config.only_today_overdue || false; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | setConfig(config) { 76 | this.config = config; 77 | } 78 | 79 | configChanged(config) { 80 | const e = new Event('config-changed', { 81 | bubbles: true, 82 | composed: true, 83 | }); 84 | 85 | e.detail = {config: config}; 86 | 87 | this.dispatchEvent(e); 88 | } 89 | 90 | getEntitiesByType(type) { 91 | return this.hass 92 | ? Object.keys(this.hass.states).filter(entity => entity.substr(0, entity.indexOf('.')) === type) 93 | : []; 94 | } 95 | 96 | isNumeric(v) { 97 | return !isNaN(parseFloat(v)) && isFinite(v); 98 | } 99 | 100 | valueChanged(e) { 101 | if ( 102 | !this.config 103 | || !this.hass 104 | || (this[`_${e.target.configValue}`] === e.target.value) 105 | ) { 106 | return; 107 | } 108 | 109 | if (e.target.configValue) { 110 | if (e.target.value === '') { 111 | if (!['entity', 'show_completed'].includes(e.target.configValue)) { 112 | delete this.config[e.target.configValue]; 113 | } 114 | } else { 115 | this.config = { 116 | ...this.config, 117 | [e.target.configValue]: e.target.checked !== undefined 118 | ? e.target.checked 119 | : this.isNumeric(e.target.value) ? parseFloat(e.target.value) : e.target.value, 120 | }; 121 | } 122 | } 123 | 124 | this.configChanged(this.config); 125 | } 126 | 127 | render() { 128 | if (!this.hass) { 129 | return html``; 130 | } 131 | 132 | const entities = this.getEntitiesByType('sensor'); 133 | const completedCount = [...Array(16).keys()]; 134 | 135 | return html`