├── .eslintrc.js
├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── README.md
├── doc
└── images
│ ├── absences-card.png
│ ├── averages-card.png
│ ├── delays-card.png
│ ├── evaluations-card.png
│ ├── grades-card.png
│ ├── homework-card.png
│ └── timetable-card.png
├── elements
├── formfield.js
├── ignore
│ ├── select.js
│ ├── switch.js
│ └── textfield.js
├── select.js
├── switch.js
└── textfield.js
├── hacs.json
├── package.json
├── rollup-plugins
└── ignore.js
├── rollup.config.dev.js
├── rollup.config.js
├── src
├── cards
│ ├── absences-card.js
│ ├── averages-card.js
│ ├── base-card.js
│ ├── base-period-related-card.js
│ ├── delays-card.js
│ ├── evaluations-card.js
│ ├── grades-card.js
│ ├── homework-card.js
│ └── timetable-card.js
├── editors
│ ├── absences-card-editor.js
│ ├── averages-card-editor.js
│ ├── base-editor.js
│ ├── delays-card-editor.js
│ ├── evaluations-card-editor.js
│ ├── grades-card-editor.js
│ ├── homework-card-editor.js
│ └── timetable-card-editor.js
└── pronote.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser
3 | parserOptions: {
4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
5 | sourceType: "module" // Allows for the use of imports
6 | },
7 | extends: [
8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin
9 | ],
10 | rules: {
11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
13 | }
14 | };
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: 'Build'
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | name: Test build
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v1
17 | - name: Build
18 | run: |
19 | npm install
20 | npm run build
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | release:
9 | name: Prepare release
10 | permissions:
11 | contents: write
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v1
15 |
16 | # Build
17 | - name: Build the file
18 | run: |
19 | npm install
20 | npm run build
21 |
22 | # Upload build file to the releas as an asset.
23 | - name: Upload built package to release
24 | uses: svenstaro/upload-release-action@v1-release
25 | with:
26 | repo_token: ${{ secrets.GITHUB_TOKEN }}
27 | file: dist/pronote.js
28 | asset_name: pronote.js
29 | tag: ${{ github.ref }}
30 | overwrite: true
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /.rpt2_cache/
3 | package-lock.json
4 | /dist
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lovelace cards for the Pronote integration
2 |
3 | A few cards to help display informations from the [Pronote integration for Home Assistant](https://github.com/delphiki/hass-pronote)
4 |
5 | ## Installation
6 |
7 | ### Using HACS
8 |
9 | Add this repository to HACS : https://github.com/delphiki/lovelace-pronote.git
10 | then:
11 | HACS > Lovelace > **Pronote Cards**
12 |
13 | ## Cards
14 |
15 | ### Timetable
16 |
17 | .
18 |
19 | ```yaml
20 | type: custom:pronote-timetable-card
21 | entity: sensor.pronote_XXXX_YYYY_timetable_next_day
22 | display_header: true
23 | display_lunch_break: true
24 | display_classroom: true
25 | display_teacher: true
26 | display_day_hours: true
27 | dim_ended_lessons: true
28 | max_days: null
29 | current_week_only: false
30 | ```
31 |
32 | This card can be used with all timetable sensors.
33 |
34 | ### Homework
35 |
36 | .
37 |
38 | ```yaml
39 | type: custom:pronote-homework-card
40 | entity: sensor.pronote_XXXX_YYYY_homework
41 | display_header: true
42 | display_done_homework: true
43 | reduce_done_homework: true
44 | current_week_only: false
45 | ```
46 |
47 | This card can be used with all homework sensors.
48 |
49 | ### Grades
50 |
51 | .
52 |
53 | ```yaml
54 | type: custom:pronote-grades-card
55 | entity: sensor.pronote_XXXX_YYYY_grades
56 | grade_format: full # 'full' will display grade as "X/Y", 'short' will display "X"
57 | display_header: true
58 | display_date: true
59 | display_comment: true
60 | display_class_average: true
61 | compare_with_class_average: true
62 | compare_with_ratio: null # use a float number, e.g. '0.6' to compare with the grade / out_of ratio
63 | display_coefficient: true
64 | display_class_min: true
65 | display_class_max: true
66 | display_new_grade_notice: true
67 | max_grades: null
68 | ```
69 |
70 | ### Averages
71 |
72 | .
73 |
74 | ```yaml
75 | type: custom:pronote-averages-card
76 | entity: sensor.pronote_XXXX_YYYY_averages
77 | average_format: full # 'full' will display grade as "X/Y", 'short' will display "X"
78 | display_header: true
79 | compare_with_class_average: true
80 | compare_with_ratio: null # use a float number, e.g. '0.6' to compare with the grade / out_of ratio
81 | display_class_average: true
82 | display_class_min: true
83 | display_class_max: true
84 | ```
85 |
86 | ### Evaluations
87 |
88 | .
89 |
90 | ```yaml
91 | type: custom:pronote-evaluations-card
92 | entity: sensor.pronote_XXXX_YYYY_evaluations
93 | display_header: true
94 | display_description: true
95 | display_teacher: true
96 | display_date: true
97 | display_comment: true
98 | display_coefficient: true
99 | max_evaluations: null
100 | child_name: null
101 | ```
102 |
103 | ### Absences
104 |
105 | .
106 |
107 | ```yaml
108 | type: custom:pronote-absences-card
109 | entity: sensor.pronote_XXXX_YYYY_absences
110 | display_header: true
111 | max_absences: null
112 | child_name: null
113 | ```
114 |
115 | ### Delays
116 |
117 | .
118 |
119 | ```yaml
120 | type: custom:pronote-delays-card
121 | entity: sensor.pronote_XXXX_YYYY_delays
122 | display_header: true
123 | max_delays: null
124 | child_name: null
125 | ```
--------------------------------------------------------------------------------
/doc/images/absences-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/absences-card.png
--------------------------------------------------------------------------------
/doc/images/averages-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/averages-card.png
--------------------------------------------------------------------------------
/doc/images/delays-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/delays-card.png
--------------------------------------------------------------------------------
/doc/images/evaluations-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/evaluations-card.png
--------------------------------------------------------------------------------
/doc/images/grades-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/grades-card.png
--------------------------------------------------------------------------------
/doc/images/homework-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/homework-card.png
--------------------------------------------------------------------------------
/doc/images/timetable-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delphiki/lovelace-pronote/72f221975e4c93af93be34f7ceb0fe6e82e03457/doc/images/timetable-card.png
--------------------------------------------------------------------------------
/elements/formfield.js:
--------------------------------------------------------------------------------
1 | import { FormfieldBase } from '@material/mwc-formfield/mwc-formfield-base.js';
2 | import { styles as formfieldStyles } from '@material/mwc-formfield/mwc-formfield.css.js';
3 |
4 | export const formfieldDefinition = {
5 | 'mwc-formfield': class extends FormfieldBase {
6 | static get styles() {
7 | return formfieldStyles;
8 | }
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/elements/ignore/select.js:
--------------------------------------------------------------------------------
1 | export const ignoreSelectFiles = [
2 | '@material/mwc-ripple/mwc-ripple.js',
3 | '@material/mwc-menu/mwc-menu.js',
4 | '@material/mwc-menu/mwc-menu-surface.js',
5 | '@material/mwc-list/mwc-list.js',
6 | '@material/mwc-list/mwc-list-item.js',
7 | '@material/mwc-icon/mwc-icon.js',
8 | ];
9 |
--------------------------------------------------------------------------------
/elements/ignore/switch.js:
--------------------------------------------------------------------------------
1 | export const ignoreSwitchFiles = ['@material/mwc-ripple/mwc-ripple.js'];
2 |
--------------------------------------------------------------------------------
/elements/ignore/textfield.js:
--------------------------------------------------------------------------------
1 | export const ignoreTextfieldFiles = ['@material/mwc-notched-outline/mwc-notched-outline.js'];
2 |
--------------------------------------------------------------------------------
/elements/select.js:
--------------------------------------------------------------------------------
1 | import { SelectBase } from '@material/mwc-select/mwc-select-base.js';
2 | import { ListBase } from '@material/mwc-list/mwc-list-base.js';
3 | import { ListItemBase } from '@material/mwc-list/mwc-list-item-base.js';
4 | import { MenuBase } from '@material/mwc-menu/mwc-menu-base.js';
5 | import { MenuSurfaceBase } from '@material/mwc-menu/mwc-menu-surface-base.js';
6 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base.js';
7 | import { NotchedOutlineBase } from '@material/mwc-notched-outline/mwc-notched-outline-base.js';
8 |
9 | import { styles as selectStyles } from '@material/mwc-select/mwc-select.css';
10 | import { styles as listStyles } from '@material/mwc-list/mwc-list.css';
11 | import { styles as listItemStyles } from '@material/mwc-list//mwc-list-item.css';
12 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css';
13 | import { styles as menuStyles } from '@material/mwc-menu/mwc-menu.css';
14 | import { styles as menuSurfaceStyles } from '@material/mwc-menu/mwc-menu-surface.css';
15 | import { styles as notchedOutlineStyles } from '@material/mwc-notched-outline/mwc-notched-outline.css';
16 |
17 | export const selectDefinition = {
18 | 'mwc-select': class extends SelectBase {
19 | static get styles() {
20 | return selectStyles;
21 | }
22 | },
23 | 'mwc-list': class extends ListBase {
24 | static get styles() {
25 | return listStyles;
26 | }
27 | },
28 | 'mwc-list-item': class extends ListItemBase {
29 | static get styles() {
30 | return listItemStyles;
31 | }
32 | },
33 | 'mwc-ripple': class extends RippleBase {
34 | static get styles() {
35 | return rippleStyles;
36 | }
37 | },
38 | 'mwc-menu': class extends MenuBase {
39 | static get styles() {
40 | return menuStyles;
41 | }
42 | },
43 | 'mwc-menu-surface': class extends MenuSurfaceBase {
44 | static get styles() {
45 | return menuSurfaceStyles;
46 | }
47 | },
48 | 'mwc-notched-outline': class extends NotchedOutlineBase {
49 | static get styles() {
50 | return notchedOutlineStyles;
51 | }
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/elements/switch.js:
--------------------------------------------------------------------------------
1 | import { SwitchBase } from '@material/mwc-switch/deprecated/mwc-switch-base.js';
2 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base.js';
3 | import { styles as switchStyles } from '@material/mwc-switch/deprecated/mwc-switch.css';
4 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css';
5 |
6 | export const switchDefinition = {
7 | 'mwc-switch': class extends SwitchBase {
8 | static get styles() {
9 | return switchStyles;
10 | }
11 | },
12 | 'mwc-ripple': class extends RippleBase {
13 | static get styles() {
14 | return rippleStyles;
15 | }
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/elements/textfield.js:
--------------------------------------------------------------------------------
1 | import { TextFieldBase } from '@material/mwc-textfield/mwc-textfield-base.js';
2 | import { NotchedOutlineBase } from '@material/mwc-notched-outline/mwc-notched-outline-base.js';
3 |
4 | import { styles as textfieldStyles } from '@material/mwc-textfield/mwc-textfield.css';
5 | import { styles as notchedOutlineStyles } from '@material/mwc-notched-outline/mwc-notched-outline.css';
6 |
7 | export const textfieldDefinition = {
8 | 'mwc-textfield': class extends TextFieldBase {
9 | static get styles() {
10 | return textfieldStyles;
11 | }
12 | },
13 | 'mwc-notched-outline': class extends NotchedOutlineBase {
14 | static get styles() {
15 | return notchedOutlineStyles;
16 | }
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pronote Cards",
3 | "filename": "pronote.js",
4 | "render_readme": true
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lovelace-pronote",
3 | "version": "1.0.1",
4 | "description": "Lovelace cards for Pronote",
5 | "main": "dist/pronote.js",
6 | "scripts": {
7 | "build": "npm run lint && npm run rollup",
8 | "rollup": "rollup -c",
9 | "babel": "babel dist/pronote.js --out-file dist/pronote.js",
10 | "lint": "eslint src/*.ts",
11 | "watch": "rollup -c --watch",
12 | "postversion": "npm run build",
13 | "audit-fix": "npx yarn-audit-fix"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/delphiki/lovelace-pronote.git"
18 | },
19 | "keywords": [
20 | "lovelace"
21 | ],
22 | "author": "delphiki",
23 | "contributors": [
24 | "delphiki (https://github.com/delphiki)"
25 | ],
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/delphiki/lovelace-pronote/issues"
29 | },
30 | "homepage": "https://github.com/delphiki/lovelace-pronote#readme",
31 | "devDependencies": {
32 | "@babel/core": "^7.12.3",
33 | "@babel/plugin-proposal-class-properties": "^7.12.1",
34 | "@babel/plugin-proposal-decorators": "^7.12.1",
35 | "@rollup/plugin-babel": "^5.2.1",
36 | "@rollup/plugin-commonjs": "^16.0.0",
37 | "@rollup/plugin-json": "^4.0.2",
38 | "@rollup/plugin-node-resolve": "^10.0.0",
39 | "@semantic-release/changelog": "^5.0.1",
40 | "@semantic-release/commit-analyzer": "^8.0.1",
41 | "@semantic-release/exec": "^5.0.0",
42 | "@semantic-release/git": "^9.0.0",
43 | "@semantic-release/npm": "^7.0.10",
44 | "@semantic-release/release-notes-generator": "^9.0.1",
45 | "@typescript-eslint/eslint-plugin": "^6.1.0",
46 | "@typescript-eslint/parser": "^6.1.0",
47 | "conventional-changelog-conventionalcommits": "^4.5.0",
48 | "eslint": "7.12.1",
49 | "eslint-config-airbnb-base": "^14.1.0",
50 | "eslint-config-prettier": "^6.15.0",
51 | "eslint-plugin-import": "^2.22.1",
52 | "eslint-plugin-prettier": "^3.1.2",
53 | "npm": "^6.14.3",
54 | "prettier": "^2.1.2",
55 | "prettier-eslint": "^11.0.0",
56 | "rollup": "^2.33.1",
57 | "rollup-plugin-cleanup": "^3.2.1",
58 | "rollup-plugin-serve": "^1.1.0",
59 | "rollup-plugin-terser": "^7.0.2",
60 | "rollup-plugin-typescript2": "^0.29.0",
61 | "semantic-release": "^17.3.8",
62 | "ts-lit-plugin": "^1.1.10",
63 | "typescript": "^4.0.5",
64 | "typescript-styled-plugin": "^0.15.0",
65 | "yarn-audit-fix": "^9.3.10"
66 | },
67 | "dependencies": {
68 | "@ctrl/tinycolor": "^3.1.6",
69 | "@material/mwc-ripple": "^0.19.1",
70 | "fast-copy": "^2.1.0",
71 | "home-assistant-js-websocket": "^8.2.0",
72 | "lit": "^2.7.6",
73 | "lit-element": "^3.3.2",
74 | "lit-html": "^2.7.5",
75 | "memoize-one": "^6.0.0"
76 | }
77 | }
--------------------------------------------------------------------------------
/rollup-plugins/ignore.js:
--------------------------------------------------------------------------------
1 | export default function (userOptions = {}) {
2 | // Files need to be absolute paths.
3 | // This only works if the file has no exports
4 | // and only is imported for its side effects
5 | const files = userOptions.files || [];
6 |
7 | if (files.length === 0) {
8 | return {
9 | name: 'ignore',
10 | };
11 | }
12 |
13 | return {
14 | name: 'ignore',
15 |
16 | load(id) {
17 | return files.some((toIgnorePath) => id.startsWith(toIgnorePath))
18 | ? {
19 | code: '',
20 | }
21 | : null;
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/rollup.config.dev.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import typescript from 'rollup-plugin-typescript2';
3 | import babel from 'rollup-plugin-babel';
4 | import serve from 'rollup-plugin-serve';
5 | import { terser } from 'rollup-plugin-terser';
6 | import json from '@rollup/plugin-json';
7 | import ignore from './rollup-plugins/ignore';
8 | import { ignoreTextfieldFiles } from './elements/ignore/textfield';
9 | import { ignoreSelectFiles } from './elements/ignore/select';
10 | import { ignoreSwitchFiles } from './elements/ignore/switch';
11 |
12 | export default {
13 | input: ['src/pronote.ts'],
14 | output: {
15 | dir: './dist',
16 | format: 'es',
17 | },
18 | plugins: [
19 | resolve(),
20 | typescript(),
21 | json(),
22 | babel({
23 | exclude: 'node_modules/**',
24 | }),
25 | terser(),
26 | serve({
27 | contentBase: './dist',
28 | host: '0.0.0.0',
29 | port: 5000,
30 | allowCrossOrigin: true,
31 | headers: {
32 | 'Access-Control-Allow-Origin': '*',
33 | },
34 | }),
35 | ignore({
36 | files: [...ignoreTextfieldFiles, ...ignoreSelectFiles, ...ignoreSwitchFiles].map((file) => require.resolve(file)),
37 | }),
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-typescript2';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import nodeResolve from '@rollup/plugin-node-resolve';
4 | import babel from '@rollup/plugin-babel';
5 | import { terser } from 'rollup-plugin-terser';
6 | import serve from 'rollup-plugin-serve';
7 | import json from '@rollup/plugin-json';
8 | import cleanup from 'rollup-plugin-cleanup';
9 |
10 | const dev = process.env.ROLLUP_WATCH;
11 |
12 | const serveopts = {
13 | contentBase: ['./dist'],
14 | host: '0.0.0.0',
15 | port: 5000,
16 | allowCrossOrigin: true,
17 | headers: {
18 | 'Access-Control-Allow-Origin': '*',
19 | },
20 | };
21 |
22 | const plugins = [
23 | nodeResolve({}),
24 | commonjs(),
25 | typescript(),
26 | json(),
27 | babel({
28 | exclude: 'node_modules/**',
29 | babelHelpers: 'bundled',
30 | }),
31 | cleanup({ comments: 'none' }),
32 | dev && serve(serveopts),
33 | !dev &&
34 | terser({
35 | mangle: {
36 | safari10: true,
37 | },
38 | }),
39 | ];
40 |
41 | export default [
42 | {
43 | input: 'src/pronote.ts',
44 | output: {
45 | dir: './dist',
46 | format: 'es',
47 | sourcemap: dev ? true : false,
48 | },
49 | plugins: [...plugins],
50 | watch: {
51 | exclude: 'node_modules/**',
52 | },
53 | },
54 | ];
--------------------------------------------------------------------------------
/src/cards/absences-card.js:
--------------------------------------------------------------------------------
1 | import BasePeriodRelatedPronoteCard from './base-period-related-card';
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | class PronoteAbsencesCard extends BasePeriodRelatedPronoteCard {
10 |
11 | period_sensor_key = 'absences'
12 | items_attribute_key = 'absences'
13 | header_title = 'Absences de '
14 | no_data_message = 'Aucune absence'
15 |
16 | getAbsencesRow(absence) {
17 | let from = this.getFormattedDate(absence.from);
18 | let to = this.getFormattedDate(absence.to);
19 | let content = html`
20 |
21 |
22 | ${absence.justified ? html`` : html``}
23 | |
24 | |
25 | ${from} au ${to} ${absence.hours} de cours manquées
26 | |
27 |
28 | ${absence.reason}
29 | |
30 |
31 | `
32 | return html`${content}`;
33 | }
34 |
35 | getFormattedDate(date) {
36 | return (new Date(date)) ? new Date(date).toLocaleDateString('fr-FR', { weekday: 'long', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/^(.)/, (match) => match.toUpperCase()) : '';
37 | }
38 |
39 | getCardContent() {
40 | const stateObj = this.hass.states[this.config.entity];
41 |
42 | if (stateObj) {
43 | const absences = this.getFilteredItems();
44 | const itemTemplates = [
45 | this.getPeriodSwitcher()
46 | ];
47 | let dayTemplates = [];
48 | let absencesCount = 0;
49 | for (let index = 0; index < absences.length; index++) {
50 | absencesCount++;
51 | if (this.config.max_absences && this.config.max_absences < absencesCount) {
52 | break;
53 | }
54 | let absence = absences[index];
55 | dayTemplates.push(this.getAbsencesRow(absence));
56 | }
57 |
58 | if (absencesCount > 0) {
59 | itemTemplates.push(html``);
60 | } else {
61 | itemTemplates.push(this.noDataMessage());
62 | }
63 |
64 | return itemTemplates;
65 | }
66 | }
67 |
68 | getDefaultConfig() {
69 | return {
70 | ...super.getDefaultConfig(),
71 | display_header: true,
72 | max_absences: null
73 | };
74 | }
75 |
76 | static get styles() {
77 | return css`
78 | ${super.styles}
79 | table{
80 | clear:both;
81 | font-size: 0.9em;
82 | font-family: Roboto;
83 | width: 100%;
84 | outline: 0px solid #393c3d;
85 | border-collapse: collapse;
86 | }
87 | tr:nth-child(odd) {
88 | background-color: rgba(0,0,0,0.1);
89 | }
90 | td {
91 | vertical-align: middle;
92 | padding: 5px 10px 5px 10px;
93 | text-align: left;
94 | }
95 | tr td:first-child {
96 | width: 10%;
97 | text-align:right;
98 | }
99 | span.absence-reason {
100 | font-weight:bold;
101 | display:block;
102 | }
103 | tr td:nth-child(2) {
104 | width: 4px;
105 | padding: 5px 0;
106 | }
107 | tr td:nth-child(2) > span {
108 | display:inline-block;
109 | width: 4px;
110 | height: 3rem;
111 | border-radius:4px;
112 | background-color: grey;
113 | margin-top:4px;
114 | }
115 | span.absence-from {
116 | font-weight:bold;
117 | padding: 4px;
118 | border-radius: 4px;
119 | }
120 | span.absence-hours {
121 | font-size: 0.9em;
122 | padding: 4px;
123 | }
124 | table + div {
125 | border-top: 1px solid white;
126 | }
127 | `;
128 | }
129 |
130 | static getStubConfig() {
131 | return {
132 | display_header: true,
133 | max_absences: null,
134 | }
135 | }
136 |
137 | static getConfigElement() {
138 | return document.createElement("pronote-absences-card-editor");
139 | }
140 | }
141 |
142 | customElements.define("pronote-absences-card", PronoteAbsencesCard);
143 |
144 | window.customCards = window.customCards || [];
145 | window.customCards.push({
146 | type: "pronote-absences-card",
147 | name: "Pronote Absences Card",
148 | description: "Display the absences from Pronote",
149 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#absences",
150 | });
151 |
--------------------------------------------------------------------------------
/src/cards/averages-card.js:
--------------------------------------------------------------------------------
1 | import BasePeriodRelatedPronoteCard from './base-period-related-card';
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | class PronoteAveragesCard extends BasePeriodRelatedPronoteCard {
10 |
11 | period_sensor_key = 'averages'
12 | items_attribute_key = 'averages'
13 | header_title = 'Moyennes de '
14 | no_data_message = 'Aucune moyenne'
15 | allow_all_periods = false
16 |
17 | getOverallAverageRow() {
18 | let sensor_prefix = this.config.entity.split('_'+this.period_sensor_key)[0];
19 | let overall_average_entity = `${sensor_prefix}_overall_average`;
20 |
21 | if (this.period_filter !== null && !this.isCurrentPeriodSelected()) {
22 | overall_average_entity = `${overall_average_entity}_${this.period_filter}`;
23 | }
24 |
25 | if (!this.hass.states[overall_average_entity]) {
26 | return html``;
27 | }
28 |
29 | let overall_average = this.hass.states[overall_average_entity].state;
30 |
31 | if (!overall_average) {
32 | return html``;
33 | }
34 |
35 | let average = parseFloat(overall_average.replace(',', '.'));
36 | let average_classes = [];
37 |
38 | if (this.config.compare_with_ratio !== null) {
39 | let comparison_ratio = parseFloat(this.config.compare_with_ratio);
40 | let average_ratio = average / parseFloat(overall_average.out_of.replace(',', '.'));
41 | average_classes.push(average_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio');
42 | }
43 |
44 | return html`
45 |
46 | |
47 |
48 | Moyenne générale
49 | |
50 |
51 | ${overall_average.replace('.', ',')}
52 | |
53 |
54 | `;
55 | }
56 |
57 | getAverageRow(averageData) {
58 | let average = parseFloat(averageData.average.replace(',', '.'));
59 |
60 | let average_classes = [];
61 |
62 | if (this.config.compare_with_ratio !== null) {
63 | let comparison_ratio = parseFloat(this.config.compare_with_ratio);
64 | let average_ratio = average / parseFloat(averageData.out_of.replace(',', '.'));
65 | average_classes.push(average_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio');
66 | } else if (this.config.compare_with_class_average && averageData.class) {
67 | let class_average = parseFloat(averageData.class.replace(',', '.'));
68 | average_classes.push(average > class_average ? 'above-average' : 'below-average');
69 | }
70 |
71 | let formatted_average = averageData.average+'/'+averageData.out_of;
72 | if (this.config.average_format === 'short') {
73 | formatted_average = averageData.average;
74 | }
75 |
76 | return html`
77 |
78 | |
79 |
80 | ${averageData.subject}
81 | |
82 |
83 | ${formatted_average}
84 | ${this.config.display_class_average && averageData.class ? html`Classe ${averageData.class}` : ''}
85 | ${this.config.display_class_min && averageData.min ? html`Min. ${averageData.min}` : ''}
86 | ${this.config.display_class_max && averageData.max ? html`Max. ${averageData.max}` : ''}
87 | |
88 |
89 | `;
90 | }
91 |
92 | getCardContent() {
93 | const stateObj = this.hass.states[this.config.entity];
94 |
95 | if (stateObj) {
96 | const averages = this.getFilteredItems();
97 | const itemTemplates = [
98 | this.getPeriodSwitcher()
99 | ];
100 | const averagesRows = [];
101 |
102 | if (this.config.display_overall_average) {
103 | averagesRows.push(this.getOverallAverageRow(averages));
104 | }
105 |
106 | for (let index = 0; index < averages.length; index++) {
107 | let average = averages[index];
108 | averagesRows.push(this.getAverageRow(average));
109 | }
110 |
111 | if (averagesRows.length > 0) {
112 | itemTemplates.push(html``);
113 | } else {
114 | itemTemplates.push(this.noDataMessage());
115 | }
116 |
117 | return itemTemplates;
118 | }
119 |
120 | return [];
121 | }
122 |
123 | getDefaultConfig() {
124 | return {
125 | ...super.getDefaultConfig(),
126 | average_format: 'full',
127 | display_header: true,
128 | display_class_average: true,
129 | compare_with_class_average: true,
130 | compare_with_ratio: null,
131 | display_class_min: true,
132 | display_class_max: true,
133 | display_overall_average: true,
134 | }
135 | }
136 |
137 | static get styles() {
138 | return css`
139 | ${super.styles}
140 | table {
141 | font-size: 0.9em;
142 | font-family: Roboto;
143 | width: 100%;
144 | outline: 0px solid #393c3d;
145 | border-collapse: collapse;
146 | }
147 | td {
148 | vertical-align: top;
149 | padding: 5px 10px 5px 10px;
150 | padding-top: 8px;
151 | text-align: left;
152 | }
153 | tr.overall-average {
154 | border-bottom: 1px solid #393c3d;
155 | }
156 | tr.overall-average td {
157 | text-transform: uppercase;
158 | padding-bottom: 10px;
159 | }
160 | tr.overall-average .average-value span {
161 | padding: 5px;
162 | border-radius: 5px;
163 | border: 1px solid var(--primary-text-color);
164 | }
165 | tr.overall-average tr td {
166 | padding-top: 10px;
167 | }
168 | td.average-comparison-color, td.average-color {
169 | width: 4px;
170 | padding-top: 11px;
171 | }
172 | td.average-comparison-color > span, td.average-color > span {
173 | display:inline-block;
174 | width: 4px;
175 | height: 1rem;
176 | border-radius:4px;
177 | background-color: grey;
178 | }
179 |
180 | .above-average .average-detail, .above-ratio .average-detail,
181 | .below-average .average-detail, .below-ratio .average-detail {
182 | position: relative;
183 | }
184 | .above-average span.average-value, .above-ratio span.average-value,
185 | .below-average span.average-value, .below-ratio span.average-value {
186 | padding-right: 16px;
187 | }
188 | .above-average span.average-value:before, .above-ratio span.average-value:before,
189 | .below-average span.average-value:before, .below-ratio span.average-value:before {
190 | content: ' ';
191 | display: block;
192 | width: 10px;
193 | height: 10px;
194 | border-radius: 5px;
195 | position: absolute;
196 | right: 10px;
197 | top: 13px;
198 | }
199 | .above-average span.average-value:before, .above-ratio span.average-value:before {
200 | background-color: green;
201 | }
202 | .below-average span.average-value:before, .below-ratio span.average-value:before {
203 | background-color: orange;
204 | }
205 | .average-description {
206 | padding-left: 0;
207 | }
208 | .average-subject {
209 | display: inline-block;
210 | font-weight: bold;
211 | position: relative;
212 | }
213 | .average-detail {
214 | text-align: right;
215 | }
216 | .average-value {
217 | font-weight: bold;
218 | }
219 | .average-value, .average-class-average {
220 | display:block;
221 | }
222 | .average-class-average, .average-class-min, .average-class-max {
223 | font-size: 0.9em;
224 | color: gray;
225 | }
226 | .average-class-min + .average-class-max:before {
227 | content: ' - '
228 | }
229 | `;
230 | }
231 |
232 | static getStubConfig() {
233 | return {
234 | average_format: 'full',
235 | display_header: true,
236 | display_class_average: true,
237 | compare_with_class_average: true,
238 | compare_with_ratio: null,
239 | display_class_min: true,
240 | display_class_max: true,
241 | }
242 | }
243 |
244 | static getConfigElement() {
245 | return document.createElement("pronote-averages-card-editor");
246 | }
247 | }
248 |
249 | customElements.define("pronote-averages-card", PronoteAveragesCard);
250 |
251 | window.customCards = window.customCards || [];
252 | window.customCards.push({
253 | type: "pronote-averages-card",
254 | name: "Pronote Averages Card",
255 | description: "Display the averages from Pronote",
256 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#averages",
257 | });
--------------------------------------------------------------------------------
/src/cards/base-card.js:
--------------------------------------------------------------------------------
1 | const LitElement = Object.getPrototypeOf(
2 | customElements.get("ha-panel-lovelace")
3 | );
4 | const html = LitElement.prototype.html;
5 | const css = LitElement.prototype.css;
6 |
7 | class BasePronoteCard extends LitElement {
8 |
9 | static get properties() {
10 | return {
11 | config: {},
12 | hass: {},
13 | header_title: { type: String },
14 | no_data_message: { type: String }
15 | };
16 | }
17 |
18 | getCardHeader() {
19 | let child_attributes = this.hass.states[this.config.entity].attributes;
20 | let child_name = (typeof child_attributes['nickname'] === 'string' && child_attributes['nickname'].length > 0) ? child_attributes['nickname'] : child_attributes['full_name'];
21 | return html``;
22 | }
23 |
24 | noDataMessage() {
25 | return html`${this.no_data_message}
`;
26 | }
27 |
28 | render() {
29 | if (!this.config || !this.hass) {
30 | return html``;
31 | }
32 |
33 | const stateObj = this.hass.states[this.config.entity];
34 |
35 | if (stateObj) {
36 |
37 | return html`
38 |
39 | ${this.config.display_header ? this.getCardHeader() : ''}
40 | ${this.getCardContent()}
41 | `
42 | ;
43 | }
44 | }
45 |
46 | // Définit la configuration de la carte
47 | setConfig(config) {
48 | if (!config.entity) {
49 | throw new Error('You need to define an entity');
50 | }
51 |
52 | this.config = {
53 | ...this.getDefaultConfig(),
54 | ...config
55 | };
56 | }
57 |
58 | static get styles() {
59 | return css`
60 | .pronote-card-header {
61 | text-align:center;
62 | }
63 | div {
64 | padding: 12px;
65 | font-weight:bold;
66 | font-size:1em;
67 | }
68 | .pronote-card-no-data {
69 | display:block;
70 | padding:8px;
71 | text-align: center;
72 | font-style: italic;
73 | }
74 | `;
75 | }
76 | }
77 |
78 | export default BasePronoteCard;
--------------------------------------------------------------------------------
/src/cards/base-period-related-card.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCard from "./base-card";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | class BasePeriodRelatedPronoteCard extends BasePronoteCard {
10 |
11 | period_filter = null;
12 | allow_all_periods = true;
13 | items_attribute_key = null;
14 | period_sensor_key = null;
15 |
16 | getPeriodSwitcher() {
17 | if (this.period_filter === null) {
18 | this.setPeriodFilterFromConfig(this.config.default_period);
19 | }
20 |
21 | if (this.config.hide_period_switch) {
22 | return html``;
23 | }
24 |
25 | let available_periods = [...this.getActivePeriods()];
26 | if (this.allow_all_periods) {
27 | available_periods.push({
28 | id: 'all',
29 | name: 'Tout'
30 | });
31 | }
32 | let tabs = [];
33 | for (let period of available_periods) {
34 | tabs.push(
35 | html` this.handlePeriodChange(e)}"
42 | />
43 | `
44 | );
45 | }
46 |
47 | return html`${tabs}
`;
48 | }
49 |
50 | handlePeriodChange(event) {
51 | this.period_filter = event.target.value;
52 | this.requestUpdate();
53 | }
54 |
55 | setPeriodFilterFromConfig() {
56 | if (this.config.default_period && this.period_filter === null) {
57 | if (this.config.default_period === 'current') {
58 | let active_periods = this.getActivePeriods();
59 | for (let period of active_periods) {
60 | if (period.is_current_period) {
61 | this.period_filter = period.id;
62 | break;
63 | }
64 | }
65 | }
66 | else {
67 | this.period_filter = this.config.default_period;
68 | }
69 | }
70 | this.requestUpdate();
71 | }
72 |
73 | getActivePeriodsSensor() {
74 | let sensor_prefix = this.config.entity.split('_'+this.period_sensor_key)[0];
75 | return this.hass.states[`${sensor_prefix}_active_periods`];
76 | }
77 |
78 | getActivePeriods() {
79 | return this.getActivePeriodsSensor().attributes['periods'];
80 | }
81 |
82 | getAllEntityNames() {
83 | let active_periods = this.getActivePeriods();
84 | let entities = [
85 | this.config.entity
86 | ];
87 | for (let period of active_periods) {
88 | if (!period.is_current_period) {
89 | entities.push(`${this.config.entity}_${period.id}`);
90 | }
91 | }
92 | return entities;
93 | }
94 |
95 | getFilteredItems() {
96 | if (this.period_filter === 'all' && !this.allow_all_periods) {
97 | this.period_filter = this.getActivePeriods()[this.getActivePeriods().length - 1].id;
98 | }
99 |
100 | let entity_names = this.getAllEntityNames();
101 | let items = [];
102 | for (let entity_name of entity_names) {
103 | let entity_state = this.hass.states[entity_name];
104 | if (this.period_filter === 'all' || this.period_filter === entity_state.attributes['period_key']) {
105 | items.push(...entity_state.attributes[this.items_attribute_key]);
106 | }
107 | }
108 | return items;
109 | }
110 |
111 | getCurrentPeriodKey() {
112 | let active_periods = this.getActivePeriods();
113 | for (let period of active_periods) {
114 | if (period.is_current_period) {
115 | return period.id;
116 | }
117 | }
118 | return null;
119 | }
120 |
121 | isCurrentPeriodSelected() {
122 | return this.period_filter === this.getCurrentPeriodKey();
123 | }
124 |
125 | getDefaultConfig() {
126 | return {
127 | default_period: 'current',
128 | hide_period_switch: false,
129 | }
130 | }
131 |
132 | static get styles() {
133 | return css`
134 | ${super.styles}
135 | .pronote-period-switcher {
136 | display: flex;
137 | justify-content: center;
138 | gap: 10px;
139 | padding:5px;
140 | }
141 | .pronote-period-switcher input {
142 | display: none;
143 | }
144 | .pronote-period-switcher-tab {
145 | padding: 10px;
146 | }
147 | .pronote-period-switcher-tab:hover {
148 | cursor: pointer;
149 | }
150 | .pronote-period-switcher input:checked + .pronote-period-switcher-tab {
151 | background-color: rgba(0,0,0,0.1);
152 | }
153 | `;
154 | }
155 | }
156 |
157 | export default BasePeriodRelatedPronoteCard;
--------------------------------------------------------------------------------
/src/cards/delays-card.js:
--------------------------------------------------------------------------------
1 | import BasePeriodRelatedPronoteCard from './base-period-related-card';
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | class PronoteDelaysCard extends BasePeriodRelatedPronoteCard {
10 |
11 | header_title = 'Retards de '
12 | no_data_message = 'Aucun retard'
13 | period_sensor_key = 'delays'
14 | items_attribute_key = 'delays'
15 |
16 | // Génère une ligne pour un retard donné
17 | getDelaysRow(delay) {
18 | const date = this.getFormattedDate(delay.date);
19 |
20 | const rowContent = html`
21 |
22 |
23 |
24 | ${delay.justified
25 | ? html``
26 | : html``}
27 |
28 | |
29 |
30 |
31 | |
32 |
33 | ${date}
34 |
35 | ${delay.minutes} minutes de retard
36 | |
37 |
38 | ${delay.reasons}
39 | |
40 |
41 | `;
42 |
43 | return html`${rowContent}`;
44 | }
45 |
46 | // Formate la date d'un retard
47 | getFormattedDate(date) {
48 | return (new Date(date))
49 | ? new Date(date).toLocaleDateString('fr-FR', {
50 | weekday: 'long',
51 | day: '2-digit',
52 | month: '2-digit',
53 | hour: '2-digit',
54 | minute: '2-digit'
55 | }).replace(/^(.)/, (match) => match.toUpperCase())
56 | : '';
57 | }
58 |
59 | // Génère le rendu de la carte
60 | getCardContent() {
61 | const stateObj = this.hass.states[this.config.entity];
62 |
63 | if (stateObj) {
64 |
65 | const delays = this.getFilteredItems();
66 | const itemTemplates = [
67 | this.getPeriodSwitcher()
68 | ];
69 | let dayTemplates = [];
70 | let delaysCount = 0;
71 |
72 | // Génère les lignes pour chaque retard, en respectant la limite maximale si définie
73 | for (let index = 0; index < delays.length; index++) {
74 | delaysCount++;
75 |
76 | if (this.config.max_delays && this.config.max_delays < delaysCount) {
77 | break;
78 | }
79 |
80 | const currentDelay = delays[index];
81 | dayTemplates.push(this.getDelaysRow(currentDelay));
82 | }
83 |
84 | if (dayTemplates.length > 0) {
85 | itemTemplates.push(html``);
86 | } else {
87 | itemTemplates.push(this.noDataMessage());
88 | }
89 |
90 | return itemTemplates;
91 | }
92 | }
93 |
94 | getDefaultConfig() {
95 | return {
96 | ...super.getDefaultConfig(),
97 | display_header: true,
98 | max_delays: null,
99 | }
100 | }
101 |
102 | static get styles() {
103 | return css`
104 | ${super.styles}
105 | table{
106 | clear:both;
107 | font-size: 0.9em;
108 | font-family: Roboto;
109 | width: 100%;
110 | outline: 0px solid #393c3d;
111 | border-collapse: collapse;
112 | }
113 | tr:nth-child(odd) {
114 | background-color: rgba(0,0,0,0.1);
115 | }
116 | td {
117 | vertical-align: middle;
118 | padding: 5px 10px 5px 10px;
119 | text-align: left;
120 | }
121 | tr td:first-child {
122 | width: 13%;
123 | text-align:right;
124 | }
125 | span.delay-reason {
126 | font-weight:bold;
127 | display:block;
128 | }
129 | tr td:nth-child(2) {
130 | width: 4px;
131 | padding: 5px 0;
132 | }
133 | tr td:nth-child(2) > span {
134 | display:inline-block;
135 | width: 4px;
136 | height: 3rem;
137 | border-radius:4px;
138 | background-color: grey;
139 | margin-top:4px;
140 | }
141 | span.delay-from {
142 | color: white;
143 | font-weight:bold;
144 | padding: 4px;
145 | border-radius: 4px;
146 | }
147 | span.delay-hours {
148 | font-size: 0.9em;
149 | padding: 4px;
150 | }
151 | table + div {
152 | border-top: 1px solid white;
153 | }
154 | `;
155 | }
156 |
157 | static getStubConfig() {
158 | return {
159 | display_header: true,
160 | max_delays: null,
161 | }
162 | }
163 |
164 | static getConfigElement() {
165 | return document.createElement("pronote-delays-card-editor");
166 | }
167 | }
168 |
169 | customElements.define("pronote-delays-card", PronoteDelaysCard);
170 |
171 | window.customCards = window.customCards || [];
172 | window.customCards.push({
173 | type: "pronote-delays-card",
174 | name: "Pronote Delays Card",
175 | description: "Display the delays from Pronote",
176 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#delays",
177 | });
--------------------------------------------------------------------------------
/src/cards/evaluations-card.js:
--------------------------------------------------------------------------------
1 | import BasePeriodRelatedPronoteCard from "./base-period-related-card";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | class PronoteEvaluationsCard extends BasePeriodRelatedPronoteCard {
10 |
11 | header_title = 'Evaluations de '
12 | no_data_message = 'Pas d\'évaluation à afficher'
13 | period_sensor_key = 'evaluations'
14 | items_attribute_key = 'evaluations'
15 |
16 | getFormattedDate(date) {
17 | return (new Date(date))
18 | .toLocaleDateString('fr-FR', {weekday: 'short', day: '2-digit', month: '2-digit'})
19 | .replace(/^(.)/, (match) => match.toUpperCase())
20 | ;
21 | }
22 |
23 | getAcquisitionRow(acquisition) {
24 | return html`
25 | ${acquisition.name} |
26 | ${this.getAcquisitionIcon(acquisition)} |
27 |
`;
28 | }
29 |
30 | getAcquisitionIcon(acquisition) {
31 | const remappedEvaluations = this.config.mapping_evaluations[acquisition.abbreviation] || acquisition.abbreviation;
32 | let icon = '';
33 | if (remappedEvaluations === 'A+') {
34 | icon = '+';
35 | } else if (remappedEvaluations === 'Abs') {
36 | icon = 'a';
37 | }
38 | return html`
39 |
40 | ${icon}
41 |
42 | `;
43 | }
44 |
45 | getEvaluationRow(evaluation, index, lessons_colors) {
46 | let acquisitions = evaluation.acquisitions;
47 | let acquisitionIcons = [];
48 | let acquisitionsRows = [];
49 | let lesson_background_color = lessons_colors[evaluation.subject];
50 |
51 | for (let i = 0; i < acquisitions.length; i++) {
52 | acquisitionIcons.push(this.getAcquisitionIcon(acquisitions[i]));
53 | acquisitionsRows.push(this.getAcquisitionRow(acquisitions[i]));
54 | }
55 |
56 | return html`
57 |
58 | |
59 |
60 |
63 | ${this.config.display_teacher ? html`${evaluation.teacher}` : ''}
64 |
65 | ${this.config.display_comment ? html`` : ''}
66 | ${this.config.display_description ? html`${evaluation.description}` : ''}
67 | ${this.config.display_date ? html`${this.getFormattedDate(evaluation.date)}`: ''}
68 | ${this.config.display_coefficient && evaluation.coefficient ? html`Coef. ${evaluation.coefficient}` : ''}
69 | |
70 |
71 | ${acquisitionIcons}
72 | |
73 |
74 | ${acquisitionsRows}
75 | `;
76 | }
77 |
78 | getCardContent() {
79 | const stateObj = this.hass.states[this.config.entity];
80 |
81 | if (stateObj) {
82 | const evaluations = this.getFilteredItems();
83 | const max_evaluations = this.config.max_evaluations ?? evaluations.length;
84 |
85 | const evaluationsRows = [];
86 | const itemTemplates = [
87 | this.getPeriodSwitcher()
88 | ];
89 |
90 | // Va chercher les couleurs des matières
91 | const lessons = this.hass.states[this.config.entity.replace("_evaluations", "_period_s_timetable")].attributes['lessons']
92 | var lessons_colors = {};
93 | if (lessons) {
94 | for (let index = 0; index < lessons.length; index++) {
95 | let lesson = lessons[index];
96 | lessons_colors[lesson.lesson]=lesson.background_color;
97 | }
98 | }
99 |
100 | for (let index = 0; index < max_evaluations; index++) {
101 | let evaluation = evaluations[index];
102 | evaluationsRows.push(this.getEvaluationRow(evaluation, index, lessons_colors));
103 | }
104 |
105 | if (evaluationsRows.length > 0) {
106 | itemTemplates.push(html``);
107 | } else {
108 | itemTemplates.push(this.noDataMessage());
109 | }
110 |
111 | return itemTemplates;
112 | }
113 |
114 | return [];
115 | }
116 |
117 | getDefaultConfig() {
118 | return {
119 | ...super.getDefaultConfig(),
120 | display_header: true,
121 | display_description: true,
122 | display_teacher: true,
123 | display_date: true,
124 | display_comment: true,
125 | display_coefficient: true,
126 | max_evaluations: null,
127 | mapping_evaluations: {},
128 | }
129 | }
130 |
131 | static get styles() {
132 | return css`
133 | ${super.styles}
134 | table {
135 | font-size: 0.9em;
136 | font-family: Roboto;
137 | width: 100%;
138 | outline: 0px solid #393c3d;
139 | border-collapse: collapse;
140 | }
141 | td {
142 | vertical-align: top;
143 | padding: 5px 10px 5px 10px;
144 | text-align: left;
145 | }
146 | td.evaluation-color {
147 | width: 4px;
148 | padding-top: 11px;
149 | }
150 | td.evaluation-color > span {
151 | display:inline-block;
152 | width: 4px;
153 | height: 2rem;
154 | border-radius:4px;
155 | background-color: grey;
156 | }
157 | .above-average .evaluation-color > span, .above-ratio .evaluation-color > span {
158 | background-color: green;
159 | }
160 | .below-average .evaluation-color > span, .below-ratio .evaluation-color > span {
161 | background-color: orange;
162 | }
163 | .evaluation-subject {
164 | font-weight: bold;
165 | display: block;
166 | }
167 | .evaluation-description {
168 | display: block;
169 | }
170 | .evaluation-teacher {
171 | display: block;
172 | }
173 | .evaluation-date, .evaluation-coefficient {
174 | font-size: 0.9em;
175 | color: gray;
176 | }
177 | .evaluation-comment {
178 | display: block;
179 | }
180 | .evaluation-date + .evaluation-coefficient:before {
181 | content: ' - '
182 | }
183 | .evaluation-detail {
184 | text-align: right;
185 | }
186 | .evaluation-value {
187 | font-weight: bold;
188 | }
189 | .acquisition-icon {
190 | display: inline-block;
191 | width:14px;
192 | height:14px;
193 | border-radius:50%;
194 | border: solid 0.02em rgba(0, 0, 0, 0.5);
195 | margin-left: 4px;
196 | vertical-align: middle;
197 | color: white;
198 | content: '+';
199 | text-align:center;
200 | line-height:14px;
201 | }
202 | .acquisition-icon-Aplus {
203 | background-color: #008000;
204 | }
205 | .acquisition-icon-A {
206 | background-color: #45B851;
207 | }
208 | .acquisition-icon-B {
209 | background-color: ;
210 | }
211 | .acquisition-icon-C {
212 | background-color: #FFDA01;
213 | }
214 | .acquisition-icon-D {
215 | background-color: #F80A0A;
216 | }
217 | .acquisition-icon-E {
218 | background-color: #F80A0A;
219 | }
220 | .acquisition-row {
221 | display: none;
222 | }
223 | input[type="checkbox"] {
224 | display: none;
225 | }
226 | /** FIXME
227 | .evaluation-row:has(input:checked) .acquisition-icon {
228 | display:none;
229 | }
230 | .evaluation-row:has(input:checked) + .acquisition-row {
231 | display: table-row;
232 | }
233 | */
234 | .acquisition-row td:nth-child(2) {
235 | text-align: right;
236 | }
237 | `;
238 | }
239 |
240 | static getStubConfig() {
241 | return {
242 | display_header: true,
243 | display_description: true,
244 | display_teacher: true,
245 | display_date: true,
246 | display_comment: true,
247 | display_coefficient: true,
248 | max_evaluations: null,
249 | mapping_evaluations: {},
250 | }
251 | }
252 |
253 | static getConfigElement() {
254 | return document.createElement("pronote-evaluations-card-editor");
255 | }
256 | }
257 |
258 | customElements.define("pronote-evaluations-card", PronoteEvaluationsCard);
259 |
260 | window.customCards = window.customCards || [];
261 | window.customCards.push({
262 | type: "pronote-evaluations-card",
263 | name: "Pronote Evaluations Card",
264 | description: "Display the evaluations from Pronote",
265 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#evaluations",
266 | });
--------------------------------------------------------------------------------
/src/cards/grades-card.js:
--------------------------------------------------------------------------------
1 | import BasePeriodRelatedPronoteCard from './base-period-related-card';
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 | const css = LitElement.prototype.css;
9 |
10 | class PronoteGradesCard extends BasePeriodRelatedPronoteCard {
11 |
12 | header_title = 'Notes de '
13 | no_data_message = 'Aucune note disponible'
14 | period_sensor_key = 'grades'
15 | items_attribute_key = 'grades'
16 |
17 | getFormattedDate(date) {
18 | return (new Date(date))
19 | .toLocaleDateString('fr-FR', {weekday: 'short', day: '2-digit', month: '2-digit'})
20 | .replace(/^(.)/, (match) => match.toUpperCase())
21 | ;
22 | }
23 |
24 | getGradeRow(gradeData) {
25 | let grade = parseFloat(gradeData.grade.replace(',', '.'));
26 |
27 | let grade_classes = [];
28 |
29 | if (this.config.compare_with_ratio !== null) {
30 | let comparison_ratio = parseFloat(this.config.compare_with_ratio);
31 | let grade_ratio = grade / parseFloat(gradeData.out_of.replace(',', '.'));
32 | grade_classes.push(grade_ratio >= comparison_ratio ? 'above-ratio' : 'below-ratio');
33 | } else if (this.config.compare_with_class_average && gradeData.class_average) {
34 | let class_average = parseFloat(gradeData.class_average.replace(',', '.'));
35 | grade_classes.push(grade > class_average ? 'above-average' : 'below-average');
36 | }
37 |
38 | let formatted_grade = gradeData.grade_out_of;
39 | if (this.config.grade_format === 'short') {
40 | formatted_grade = gradeData.grade;
41 | }
42 |
43 | if (this.config.display_new_grade_notice) {
44 | let grade_date = new Date(gradeData.date);
45 | let today = new Date();
46 | if (
47 | grade_date.getFullYear() === today.getFullYear()
48 | && grade_date.getMonth() === today.getMonth()
49 | && grade_date.getDate() === today.getDate()
50 | ) {
51 | grade_classes.push('new-grade');
52 | }
53 | }
54 |
55 | return html`
56 |
57 | |
58 |
59 | ${gradeData.subject}
60 | ${this.config.display_comment ? html`` : ''}
61 | ${this.config.display_date ? html`${this.getFormattedDate(gradeData.date)}`: ''}
62 | ${this.config.display_coefficient && gradeData.coefficient ? html`Coef. ${gradeData.coefficient}` : ''}
63 | |
64 |
65 | ${formatted_grade}
66 | ${this.config.display_class_average && gradeData.class_average ? html`Moy. ${gradeData.class_average}` : ''}
67 | ${this.config.display_class_min && gradeData.min ? html`Min. ${gradeData.min}` : ''}
68 | ${this.config.display_class_max && gradeData.max ? html`Max. ${gradeData.max}` : ''}
69 | |
70 |
71 | `;
72 | }
73 |
74 | getCardContent() {
75 | const stateObj = this.hass.states[this.config.entity];
76 | const grades = this.getFilteredItems();
77 | const max_grades = this.config.max_grades ?? grades.length;
78 |
79 | if (stateObj) {
80 |
81 | const gradesRows = [];
82 | const itemTemplates = [
83 | this.getPeriodSwitcher()
84 | ];
85 |
86 |
87 | for (let index = 0; index < max_grades; index++) {
88 | let grade = grades[index];
89 | gradesRows.push(this.getGradeRow(grade));
90 | }
91 |
92 | if (gradesRows.length > 0) {
93 | itemTemplates.push(html``);
94 | } else {
95 | itemTemplates.push(this.noDataMessage());
96 | }
97 |
98 | return itemTemplates;
99 | }
100 | }
101 |
102 | getDefaultConfig() {
103 | return {
104 | ...super.getDefaultConfig(),
105 | grade_format: 'full',
106 | display_header: true,
107 | display_date: true,
108 | display_comment: true,
109 | display_class_average: true,
110 | compare_with_class_average: true,
111 | compare_with_ratio: null,
112 | display_coefficient: true,
113 | display_class_min: true,
114 | display_class_max: true,
115 | display_new_grade_notice: true,
116 | max_grades: null,
117 | };
118 | }
119 |
120 | static get styles() {
121 | return css`
122 | ${super.styles}
123 | table {
124 | font-size: 0.9em;
125 | font-family: Roboto;
126 | width: 100%;
127 | outline: 0px solid #393c3d;
128 | border-collapse: collapse;
129 | }
130 | td {
131 | vertical-align: top;
132 | padding: 5px 10px 5px 10px;
133 | padding-top: 8px;
134 | text-align: left;
135 | }
136 | td.grade-color {
137 | width: 4px;
138 | padding-top: 11px;
139 | }
140 | td.grade-color > span {
141 | display:inline-block;
142 | width: 4px;
143 | height: 1rem;
144 | border-radius:4px;
145 | background-color: grey;
146 | }
147 | .above-average .grade-color > span, .above-ratio .grade-color > span {
148 | background-color: green;
149 | }
150 | .below-average .grade-color > span, .below-ratio .grade-color > span {
151 | background-color: orange;
152 | }
153 | .grade-description {
154 | padding-left: 0;
155 | }
156 | .grade-subject {
157 | display: inline-block;
158 | font-weight: bold;
159 | position: relative;
160 | }
161 | .new-grade .grade-subject:after {
162 | content: " ";
163 | display: block;
164 | width: 6px;
165 | height: 6px;
166 | border-radius: 6px;
167 | background-color: orange;
168 | position: absolute;
169 | top: 6px;
170 | right: -14px;
171 | }
172 | .grade-comment {
173 | display: block;
174 | }
175 | .grade-date, .grade-coefficient {
176 | font-size: 0.9em;
177 | color: gray;
178 | }
179 | .grade-date + .grade-coefficient:before {
180 | content: ' - '
181 | }
182 | .grade-detail {
183 | text-align: right;
184 | }
185 | .grade-value {
186 | font-weight: bold;
187 | }
188 | .grade-value, .grade-class-average {
189 | display:block;
190 | }
191 | .grade-class-average, .grade-class-min, .grade-class-max {
192 | font-size: 0.9em;
193 | color: gray;
194 | }
195 | .grade-class-min + .grade-class-max:before {
196 | content: ' - '
197 | }
198 | `;
199 | }
200 |
201 | static getStubConfig() {
202 | return {
203 | grade_format: 'full',
204 | display_header: true,
205 | display_date: true,
206 | display_comment: true,
207 | display_class_average: true,
208 | compare_with_class_average: true,
209 | compare_with_ratio: null,
210 | display_coefficient: true,
211 | display_class_min: true,
212 | display_class_max: true,
213 | display_new_grade_notice: true,
214 | max_grades: null,
215 | }
216 | }
217 |
218 | static getConfigElement() {
219 | return document.createElement("pronote-grades-card-editor");
220 | }
221 | }
222 |
223 | customElements.define("pronote-grades-card", PronoteGradesCard);
224 |
225 | window.customCards = window.customCards || [];
226 | window.customCards.push({
227 | type: "pronote-grades-card",
228 | name: "Pronote Grades Card",
229 | description: "Display the grades from Pronote",
230 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#grades",
231 | });
--------------------------------------------------------------------------------
/src/cards/homework-card.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCard from "./base-card"
2 | import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
3 |
4 | const LitElement = Object.getPrototypeOf(
5 | customElements.get("ha-panel-lovelace")
6 | );
7 | const html = LitElement.prototype.html;
8 | const css = LitElement.prototype.css;
9 |
10 | Date.prototype.getWeekNumber = function () {
11 | var d = new Date(+this);
12 | d.setHours(0, 0, 0, 0);
13 | d.setDate(d.getDate() + 4 - (d.getDay() || 7));
14 | return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 8.64e7) + 1) / 7);
15 | };
16 |
17 | class PronoteHomeworkCard extends BasePronoteCard {
18 |
19 | lunchBreakRendered = false;
20 |
21 | getFormattedDate(date) {
22 | return (new Date(date))
23 | .toLocaleDateString('fr-FR', {weekday: 'long', day: '2-digit', month: '2-digit'})
24 | .replace(/^(.)/, (match) => match.toUpperCase())
25 | ;
26 | }
27 |
28 | getFormattedTime(time) {
29 | return new Intl.DateTimeFormat("fr-FR", {hour:"numeric", minute:"numeric"}).format(new Date(time));
30 | }
31 |
32 | getDayHeader(homework) {
33 | return html``;
36 | }
37 |
38 | getHomeworkRow(homework, index) {
39 | let description = homework.description.trim().replace("\n", "
");
40 | let files = [];
41 | homework.files.forEach((file) => {
42 | if (file.name.trim() === '') {
43 | return;
44 | }
45 | files.push(html`➤ ${file.name}`);
46 | });
47 |
48 |
49 | return html`
50 |
51 | |
52 |
53 |
56 |
57 | ${unsafeHTML(description)}
58 | ${files.length > 0 ? html`${files}` : ''}
59 | |
60 |
61 | ${homework.done ? html`` : html``}
62 | |
63 |
64 | `;
65 | }
66 |
67 | getCardContent() {
68 |
69 | const stateObj = this.hass.states[this.config.entity];
70 | const homework = this.hass.states[this.config.entity].attributes['homework'];
71 |
72 | if (stateObj) {
73 | const currentWeekNumber = new Date().getWeekNumber();
74 | const itemTemplates = [];
75 | let dayTemplates = [];
76 |
77 | if (homework && homework.length > 0) {
78 | let latestHomeworkDay = this.getFormattedDate(homework[0].date);
79 | for (let index = 0; index < homework.length; index++) {
80 | let hw = homework[index];
81 | let currentFormattedDate = this.getFormattedDate(hw.date);
82 |
83 | if (hw.done === true && this.config.display_done_homework === false) {
84 | continue;
85 | }
86 |
87 | if (latestHomeworkDay !== currentFormattedDate) {
88 | if (dayTemplates.length > 0) {
89 | itemTemplates.push(this.getDayHeader(homework[index-1]));
90 | itemTemplates.push(html``);
91 | dayTemplates = [];
92 | }
93 |
94 | latestHomeworkDay = currentFormattedDate;
95 | }
96 |
97 | if (this.config.current_week_only && new Date(hw.date).getWeekNumber() !== currentWeekNumber) {
98 | break;
99 | }
100 |
101 | dayTemplates.push(this.getHomeworkRow(hw, index));
102 | }
103 |
104 | if (dayTemplates.length > 0 && (
105 | !this.config.current_week_only
106 | || (this.config.current_week_only && currentWeekNumber === new Date(homework[homework.length-1].date).getWeekNumber())
107 | )) {
108 | itemTemplates.push(this.getDayHeader(homework[homework.length-1]));
109 | itemTemplates.push(html``);
110 | }
111 | }
112 |
113 | if (itemTemplates.length === 0) {
114 | itemTemplates.push(this.noDataMessage());
115 | }
116 |
117 | return itemTemplates;
118 | }
119 | }
120 |
121 | setConfig(config) {
122 | if (!config.entity) {
123 | throw new Error('You need to define an entity');
124 | }
125 |
126 | const defaultConfig = {
127 | entity: null,
128 | display_header: true,
129 | current_week_only: true,
130 | reduce_done_homework: true,
131 | display_done_homework: true,
132 | }
133 |
134 | this.config = {
135 | ...defaultConfig,
136 | ...config
137 | };
138 |
139 | this.header_title = 'Devoirs de ';
140 | this.no_data_message = 'Pas de devoirs à faire';
141 | }
142 |
143 | static get styles() {
144 | return css`
145 | ${super.styles}
146 | .pronote-homework-header {
147 | border-bottom: 2px solid grey;
148 | }
149 | table{
150 | font-size: 0.9em;
151 | font-family: Roboto;
152 | width: 100%;
153 | outline: 0px solid #393c3d;
154 | border-collapse: collapse;
155 | }
156 | td {
157 | vertical-align: top;
158 | padding: 5px 10px 5px 10px;
159 | padding-top: 8px;
160 | text-align: left;
161 | }
162 | td.homework-color {
163 | width: 4px;
164 | padding-top: 11px;
165 | }
166 | td.homework-color > span {
167 | display:inline-block;
168 | width: 4px;
169 | height: 1rem;
170 | border-radius:4px;
171 | background-color: grey;
172 | }
173 | td.homework-detail {
174 | padding:0;
175 | padding-top: 8px;
176 | padding-bottom: 8px;
177 | }
178 | span.homework-subject {
179 | display:block;
180 | font-weight:bold;
181 | }
182 | span.homework-description {
183 | font-size: 0.9em;
184 | }
185 | span.homework-files {
186 | display: block;
187 | }
188 | span.homework-files .homework-file {
189 | display: inline-block;
190 | }
191 | td.homework-status {
192 | width: 5%;
193 | }
194 | .reduce-done .homework-done label:hover {
195 | cusor: pointer;
196 | }
197 | .reduce-done .homework-done .homework-description {
198 | display: none;
199 | }
200 | .reduce-done .homework-done input:checked + .homework-description {
201 | display: block;
202 | }
203 | .homework-detail input {
204 | display: none;
205 | }
206 | `;
207 | }
208 |
209 | static getStubConfig() {
210 | return {
211 | display_header: true,
212 | current_week_only: true,
213 | reduce_done_homework: true,
214 | display_done_homework: true,
215 | }
216 | }
217 |
218 | static getConfigElement() {
219 | return document.createElement("pronote-homework-card-editor");
220 | }
221 | }
222 |
223 | customElements.define("pronote-homework-card", PronoteHomeworkCard);
224 |
225 | window.customCards = window.customCards || [];
226 | window.customCards.push({
227 | type: "pronote-homework-card",
228 | name: "Pronote Homework Card",
229 | description: "Display the homework from Pronote",
230 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#homework",
231 | });
232 |
--------------------------------------------------------------------------------
/src/cards/timetable-card.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCard from "./base-card"
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 | const html = LitElement.prototype.html;
7 | const css = LitElement.prototype.css;
8 |
9 | Date.prototype.getWeekNumber = function () {
10 | var d = new Date(+this);
11 | d.setHours(0, 0, 0, 0);
12 | d.setDate(d.getDate() + 4 - (d.getDay() || 7));
13 | return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 8.64e7) + 1) / 7);
14 | };
15 |
16 | class PronoteTimetableCard extends BasePronoteCard {
17 |
18 | lunchBreakRendered = false;
19 |
20 | getBreakRow(label, ended) {
21 | return html`
22 |
23 | |
24 | |
25 |
26 | ${label}
27 | |
28 |
`;
29 | }
30 |
31 | getTimetableRow(lesson) {
32 | let currentDate = new Date();
33 | let startAt = Date.parse(lesson.start_at);
34 | let endAt = Date.parse(lesson.end_at);
35 |
36 | let prefix = html``;
37 | if (this.config.display_lunch_break && lesson.is_afternoon && !this.lunchBreakRendered) {
38 | prefix = this.getBreakRow('Repas', this.config.dim_ended_lessons && startAt < currentDate);
39 | this.lunchBreakRendered = true;
40 | }
41 |
42 | let content = html`
43 |
44 |
45 | ${lesson.start_time}
46 | ${lesson.end_time}
47 | |
48 | |
49 |
50 | ${lesson.lesson}
51 | ${this.config.display_classroom ? html`
52 | ${lesson.classroom ? 'Salle '+lesson.classroom : ''}
53 | ${lesson.classroom && this.config.display_teacher ? ', ' : '' }
54 | ` : '' }
55 | ${this.config.display_teacher ? html`
56 | ${lesson.teacher_name}
57 | `: '' }
58 | |
59 |
60 | ${lesson.status ? html`${lesson.status}`:''}
61 | |
62 |
63 | `
64 | return html`${prefix}${content}`;
65 | }
66 |
67 | getFormattedDate(lesson) {
68 | return (new Date(lesson.start_at))
69 | .toLocaleDateString('fr-FR', {weekday: 'long', day: '2-digit', month: '2-digit'})
70 | .replace(/^(.)/, (match) => match.toUpperCase())
71 | ;
72 | }
73 |
74 | getFormattedTime(time) {
75 | return new Intl.DateTimeFormat("fr-FR", {hour:"numeric", minute:"numeric"}).format(new Date(time));
76 | }
77 |
78 | getDayHeader(firstLesson, dayStartAt, dayEndAt, daysCount) {
79 | return html``;
93 | }
94 |
95 | changeDay(direction, e) {
96 | e.preventDefault();
97 | if (e.target.classList.contains('disabled')) {
98 | return;
99 | }
100 |
101 | const activeDay = e.target.parentElement.parentElement;
102 | let hasPreviousDay = activeDay.previousElementSibling && activeDay.previousElementSibling.classList.contains('pronote-timetable-day-wrapper');
103 | let hasNextDay = activeDay.nextElementSibling && activeDay.nextElementSibling.classList.contains('pronote-timetable-day-wrapper');
104 | let newActiveDay = null;
105 |
106 | if (direction === 'previous' && hasPreviousDay) {
107 | newActiveDay = activeDay.previousElementSibling;
108 | } else if (direction === 'next' && hasNextDay) {
109 | newActiveDay = activeDay.nextElementSibling;
110 | }
111 |
112 | if (newActiveDay) {
113 | activeDay.classList.remove('active');
114 | newActiveDay.classList.add('active');
115 |
116 | hasPreviousDay = newActiveDay.previousElementSibling && newActiveDay.previousElementSibling.classList.contains('pronote-timetable-day-wrapper');
117 | hasNextDay = newActiveDay.nextElementSibling && newActiveDay.nextElementSibling.classList.contains('pronote-timetable-day-wrapper');
118 |
119 | if (!hasPreviousDay) {
120 | newActiveDay.querySelector('.pronote-timetable-header-arrow-left').classList.add('disabled');
121 | }
122 |
123 | if (!hasNextDay) {
124 | newActiveDay.querySelector('.pronote-timetable-header-arrow-right').classList.add('disabled');
125 | }
126 | }
127 | }
128 |
129 | // we override the render method to return the card content
130 | render() {
131 | if (!this.config || !this.hass) {
132 | return html``;
133 | }
134 |
135 | const stateObj = this.hass.states[this.config.entity];
136 |
137 | const lessons = this.hass.states[this.config.entity].attributes['lessons']
138 |
139 | if (stateObj) {
140 | this.lunchBreakRendered = false;
141 | const currentWeekNumber = new Date().getWeekNumber();
142 |
143 | const itemTemplates = [];
144 | let dayTemplates = [];
145 | let daysCount = 0;
146 |
147 | let dayStartAt = null;
148 | let dayEndAt = null;
149 |
150 | for (let index = 0; index < lessons.length; index++) {
151 | let lesson = lessons[index];
152 | let currentFormattedDate = this.getFormattedDate(lesson);
153 |
154 | if (!lesson.canceled) {
155 | if (dayStartAt === null) {
156 | dayStartAt = lesson.start_at;
157 | }
158 | dayEndAt = lesson.end_at;
159 | }
160 |
161 | if (lesson.canceled && index < lessons.length - 1) {
162 | let nextLesson = lessons[index + 1];
163 | if (lesson.start_at === nextLesson.start_at && !nextLesson.canceled) {
164 | continue;
165 | }
166 | }
167 |
168 | if (this.config.current_week_only) {
169 | if (new Date(lesson.start_at).getWeekNumber() > currentWeekNumber) {
170 | break;
171 | }
172 | }
173 |
174 | dayTemplates.push(this.getTimetableRow(lesson));
175 |
176 | // checking if next lesson is on another day
177 | if (index + 1 >= lessons.length || ((index + 1) < lessons.length && currentFormattedDate !== this.getFormattedDate(lessons[index+1]))) {
178 | itemTemplates.push(html`
179 |
183 | `);
184 | dayTemplates = [];
185 |
186 | this.lunchBreakRendered = false;
187 | dayStartAt = null;
188 | dayEndAt = null;
189 |
190 | daysCount++;
191 | if (this.config.max_days && this.config.max_days <= daysCount) {
192 | break;
193 | }
194 | } else if (this.config.display_free_time_slots && index + 1 < lessons.length) {
195 | const currentEndAt = new Date(lesson.end_at);
196 | const nextLesson = lessons[index+1];
197 | const nextLessonStartAt = new Date(nextLesson.start_at);
198 | if (lesson.is_morning === nextLesson.is_morning && Math.floor((nextLessonStartAt-currentEndAt) / 1000 / 60) > 30) {
199 | const now = new Date();
200 | dayTemplates.push(this.getBreakRow('Pas de cours', this.config.dim_ended_lessons && nextLessonStartAt < now));
201 | }
202 | }
203 | }
204 |
205 | if (dayTemplates.length > 0) {
206 | itemTemplates.push(html``);
207 | }
208 |
209 | return html`
210 |
211 | ${this.config.display_header ? this.getCardHeader() : ''}
212 | ${itemTemplates}
213 | `
214 | ;
215 | }
216 | }
217 |
218 | setConfig(config) {
219 | if (!config.entity) {
220 | throw new Error('You need to define an entity');
221 | }
222 |
223 | const defaultConfig = {
224 | entity: null,
225 | display_header: true,
226 | display_lunch_break: true,
227 | display_classroom: true,
228 | display_teacher: true,
229 | display_day_hours: true,
230 | dim_ended_lessons: true,
231 | max_days: null,
232 | current_week_only: false,
233 | enable_slider: false,
234 | display_free_time_slots: true,
235 | }
236 |
237 | this.config = {
238 | ...defaultConfig,
239 | ...config
240 | };
241 |
242 | this.header_title = 'Emploi du temps de ';
243 | this.no_data_message = 'Pas d\'emploi du temps à afficher';
244 | }
245 |
246 | static get styles() {
247 | return css`
248 | ${super.styles}
249 | .pronote-timetable-card-slider .pronote-timetable-day-wrapper {
250 | display: none;
251 | }
252 | .pronote-timetable-card-slider .pronote-timetable-day-wrapper.active {
253 | display: block;
254 | }
255 | .pronote-timetable-card-slider .pronote-timetable-header-date {
256 | display: inline-block;
257 | text-align: center;
258 | width: 120px;
259 | }
260 | .pronote-timetable-header-arrow-left,
261 | .pronote-timetable-header-arrow-right {
262 | cursor: pointer;
263 | }
264 | .pronote-timetable-header-arrow-left.disabled,
265 | .pronote-timetable-header-arrow-right.disabled {
266 | opacity: 0.3;
267 | pointer-events: none;
268 | }
269 | span.pronote-timetable-header-hours {
270 | float:right;
271 | }
272 | table{
273 | clear:both;
274 | font-size: 0.9em;
275 | font-family: Roboto;
276 | width: 100%;
277 | outline: 0px solid #393c3d;
278 | border-collapse: collapse;
279 | }
280 | tr:nth-child(odd) {
281 | background-color: rgba(0,0,0,0.1);
282 | }
283 | td {
284 | vertical-align: middle;
285 | padding: 5px 10px 5px 10px;
286 | text-align: left;
287 | }
288 | tr td:first-child {
289 | width: 13%;
290 | text-align:right;
291 | }
292 | span.lesson-name {
293 | font-weight:bold;
294 | display:block;
295 | }
296 | tr td:nth-child(2) {
297 | width: 4px;
298 | padding: 5px 0;
299 | }
300 | tr td:nth-child(2) > span {
301 | display:inline-block;
302 | width: 4px;
303 | height: 3rem;
304 | border-radius:4px;
305 | background-color: grey;
306 | margin-top:4px;
307 | }
308 | span.lesson-status {
309 | color: white;
310 | background-color: rgb(75, 197, 253);
311 | padding: 4px;
312 | border-radius: 4px;
313 | }
314 | .lesson-canceled span.lesson-name {
315 | text-decoration: line-through;
316 | }
317 | .lesson-canceled span.lesson-status {
318 | background-color: rgb(250, 50, 75);
319 | }
320 | .lesson-ended {
321 | opacity: 0.3;
322 | }
323 | div:not(.slider-enabled).pronote-timetable-day-wrapper + div:not(.slider-enabled).pronote-timetable-day-wrapper {
324 | border-top: 1px solid white;
325 | }
326 | `;
327 | }
328 |
329 | static getStubConfig() {
330 | return {
331 | display_header: true,
332 | display_lunch_break: true,
333 | display_classroom: true,
334 | display_teacher: true,
335 | display_day_hours: true,
336 | dim_ended_lessons: true,
337 | max_days: null,
338 | current_week_only: false,
339 | enable_slider: false,
340 | display_free_time_slots: true,
341 | }
342 | }
343 |
344 | static getConfigElement() {
345 | return document.createElement("pronote-timetable-card-editor");
346 | }
347 | }
348 |
349 | customElements.define("pronote-timetable-card", PronoteTimetableCard);
350 |
351 | window.customCards = window.customCards || [];
352 | window.customCards.push({
353 | type: "pronote-timetable-card",
354 | name: "Pronote Timetable Card",
355 | description: "Display the timetable from Pronote",
356 | documentationURL: "https://github.com/delphiki/lovelace-pronote?tab=readme-ov-file#timetable",
357 | });
--------------------------------------------------------------------------------
/src/editors/absences-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteAbsencesCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Absences entity', 'entity', this._config.entity, 'absences')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildNumberField('Max absences', 'max_absences', this._config.max_absences)}
19 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)}
20 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'absences', this._config.default_period)}
21 | `;
22 | }
23 | }
24 |
25 | customElements.define("pronote-absences-card-editor", PronoteAbsencesCardEditor);
26 |
--------------------------------------------------------------------------------
/src/editors/averages-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteAveragesCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Averages entity', 'entity', this._config.entity, 'averages')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildSelectField('Average format', 'average_format', [{value: 'full', label: 'Full'}, {value: 'short', label: 'Short'}],this._config.average_format)}
19 | ${this.buildSwitchField('Display class average', 'display_class_average', this._config.display_class_average)}
20 | ${this.buildSwitchField('Compare with class average', 'compare_with_class_average', this._config.compare_with_class_average)}
21 | ${this.buildTextField('Compare with ratio', 'compare_with_ratio', this._config.compare_with_ratio, '')}
22 | ${this.buildSwitchField('Display class min', 'display_class_min', this._config.display_class_min)}
23 | ${this.buildSwitchField('Display class max', 'display_class_max', this._config.display_class_max)}
24 | ${this.buildSwitchField('Display overall average', 'display_overall_average', this._config.display_overall_average, true)}
25 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)}
26 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'averages', this._config.default_period, 'current', false)}
27 | `;
28 | }
29 | }
30 |
31 | customElements.define("pronote-averages-card-editor", PronoteAveragesCardEditor);
32 |
--------------------------------------------------------------------------------
/src/editors/base-editor.js:
--------------------------------------------------------------------------------
1 | const LitElement = Object.getPrototypeOf(
2 | customElements.get("ha-panel-lovelace")
3 | );
4 | const html = LitElement.prototype.html;
5 | const css = LitElement.prototype.css;
6 |
7 | class BasePronoteCardEditor extends LitElement {
8 | static get properties() {
9 | return {
10 | hass: {},
11 | _config: {},
12 | };
13 | }
14 |
15 | setConfig(config) {
16 | this._config = config;
17 | this.loadEntityPicker();
18 | }
19 |
20 | _valueChanged(ev) {
21 | const _config = Object.assign({}, this._config);
22 |
23 | if (typeof ev.target.__checked !== 'undefined') {
24 | _config[ev.target.configValue] = ev.target.__checked;
25 | } else {
26 | _config[ev.target.configValue] = ev.target.value == '' ? null : ev.target.value;
27 | }
28 |
29 | this._config = _config;
30 |
31 | const event = new CustomEvent("config-changed", {
32 | detail: { config: _config },
33 | bubbles: true,
34 | composed: true,
35 | });
36 | this.dispatchEvent(event);
37 | }
38 |
39 | buildSelectField(label, config_key, options, value, default_value) {
40 | let selectOptions = [];
41 | for (let i = 0; i < options.length; i++) {
42 | let currentOption = options[i];
43 | selectOptions.push(html`${currentOption.label}`);
44 | }
45 |
46 | return html`
47 | ev.stopPropagation()}
53 | >
54 | ${selectOptions}
55 |
56 | `
57 | }
58 |
59 | buildDefaultPeriodSelectField(label, config_key, period_sensor_key, value, default_value = 'current', allow_all_periods = true) {
60 | if (!this._config.entity) {
61 | return html``;
62 | }
63 | let sensor_prefix = this._config.entity.split('_'+period_sensor_key)[0];
64 | let active_periods = this.hass.states[`${sensor_prefix}_active_periods`].attributes['periods'];
65 |
66 | let options = [];
67 | if (allow_all_periods) {
68 | options.push({value:'all', label: 'All'});
69 | }
70 | options.push({value:'current', label: 'Current'});
71 | for (let period of active_periods) {
72 | options.push({value: period.id, label: period.name});
73 | }
74 |
75 | return this.buildSelectField(label, config_key, options, value, default_value);
76 | }
77 |
78 | buildSwitchField(label, config_key, value, default_value) {
79 | if (typeof value !== 'boolean') {
80 | value = default_value;
81 | }
82 |
83 | return html`
84 |
85 |
86 |
92 |
93 | `;
94 | }
95 |
96 | buildNumberField(label, config_key, value, default_value, step) {
97 | return html`
98 |
104 | `;
105 | }
106 |
107 | buildTextField(label, config_key, value, default_value) {
108 | return html`
109 |
116 | `;
117 | }
118 |
119 | buildEntityPickerField(label, config_key, value, filter) {
120 | const entityFilter = new RegExp("pronote_[a-z_]+_"+filter);
121 |
122 | return html`
123 |
133 | `
134 | }
135 |
136 | async loadEntityPicker() {
137 | if (window.customElements.get("ha-entity-picker")) {
138 | return;
139 | }
140 |
141 | const ch = await window.loadCardHelpers();
142 | const c = await ch.createCardElement({ type: "entities", entities: [] });
143 | await c.constructor.getConfigElement();
144 | }
145 |
146 | static get styles() {
147 | return css`
148 | ha-selector-boolean {
149 | display: block;
150 | padding-top: 20px;
151 | clear: right;
152 | }
153 | ha-selector-boolean > ha-switch {
154 | float: right;
155 | }
156 | ha-select, ha-textfield {
157 | clear: right;
158 | width: 100%;
159 | padding-top: 15px;
160 | }
161 | `;
162 | }
163 | }
164 |
165 | export default BasePronoteCardEditor;
--------------------------------------------------------------------------------
/src/editors/delays-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteDelaysCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Delays entity', 'entity', this._config.entity, 'delays')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildNumberField('Max delays', 'max_delays', this._config.max_delays)}
19 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)}
20 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'delays', this._config.default_period)}
21 | `;
22 | }
23 | }
24 |
25 | customElements.define("pronote-delays-card-editor", PronoteDelaysCardEditor);
26 |
--------------------------------------------------------------------------------
/src/editors/evaluations-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteEvaluationsCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Evaluations entity', 'entity', this._config.entity, 'evaluations')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildSwitchField('Display description', 'display_description', this._config.display_description)}
19 | ${this.buildSwitchField('Display teacher', 'display_teacher', this._config.display_teacher)}
20 | ${this.buildSwitchField('Display date', 'display_date', this._config.display_date)}
21 | ${this.buildSwitchField('Display comment', 'display_comment', this._config.display_comment)}
22 | ${this.buildSwitchField('Display coefficient', 'display_coefficient', this._config.display_coefficient)}
23 | ${this.buildNumberField('Max evaluations', 'max_evaluations', this._config.max_evaluations)}
24 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)}
25 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'evaluations', this._config.default_period)}
26 | `;
27 | }
28 | }
29 |
30 | customElements.define("pronote-evaluations-card-editor", PronoteEvaluationsCardEditor);
31 |
--------------------------------------------------------------------------------
/src/editors/grades-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteGradesCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Grades entity', 'entity', this._config.entity, 'grades')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildSwitchField('Display date', 'display_date', this._config.display_date)}
19 | ${this.buildSwitchField('Display comment', 'display_comment', this._config.display_comment)}
20 | ${this.buildSwitchField('Display class average', 'display_class_average', this._config.display_class_average)}
21 | ${this.buildSwitchField('Compare with class average', 'compare_with_class_average', this._config.compare_with_class_average)}
22 | ${this.buildSelectField('Grade format', 'grade_format', [{value: 'full', label: 'Full'}, {value: 'short', label: 'Short'}], this._config.grade_format)}
23 | ${this.buildSwitchField('Display coefficient', 'display_coefficient', this._config.display_coefficient)}
24 | ${this.buildSwitchField('Display class min', 'display_class_min', this._config.display_class_min)}
25 | ${this.buildSwitchField('Display class max', 'display_class_max', this._config.display_class_max)}
26 | ${this.buildSwitchField('Display new grade notice', 'display_new_grade_notice', this._config.display_new_grade_notice)}
27 | ${this.buildNumberField('Max grades', 'max_grades', this._config.max_grades)}
28 | ${this.buildSwitchField('Hide period switch', 'hide_period_switch', this._config.hide_period_switch, false)}
29 | ${this.buildDefaultPeriodSelectField('Default period', 'default_period', 'grades', this._config.default_period)}
30 | `;
31 | }
32 | }
33 |
34 | customElements.define("pronote-grades-card-editor", PronoteGradesCardEditor);
35 |
--------------------------------------------------------------------------------
/src/editors/homework-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteHomeworkCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Homework entity', 'entity', this._config.entity, 'homework')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header)}
18 | ${this.buildSwitchField('Current week only', 'current_week_only', this._config.current_week_only)}
19 | ${this.buildSwitchField('Reduce done homework', 'reduce_done_homework', this._config.reduce_done_homework)}
20 | ${this.buildSwitchField('Display done homework', 'display_done_homework', this._config.display_done_homework)}
21 | `;
22 | }
23 | }
24 |
25 | customElements.define("pronote-homework-card-editor", PronoteHomeworkCardEditor);
26 |
--------------------------------------------------------------------------------
/src/editors/timetable-card-editor.js:
--------------------------------------------------------------------------------
1 | import BasePronoteCardEditor from "./base-editor";
2 |
3 | const LitElement = Object.getPrototypeOf(
4 | customElements.get("ha-panel-lovelace")
5 | );
6 |
7 | const html = LitElement.prototype.html;
8 |
9 | class PronoteTimetableCardEditor extends BasePronoteCardEditor {
10 | render() {
11 | if (!this.hass || !this._config) {
12 | return html``;
13 | }
14 |
15 | return html`
16 | ${this.buildEntityPickerField('Timetable entity', 'entity', this._config.entity, '(period_s|today_s|tomorrow_s|next_day_s)_timetable')}
17 | ${this.buildSwitchField('Display header', 'display_header', this._config.display_header, true)}
18 | ${this.buildSwitchField('Current week only', 'current_week_only', this._config.current_week_only, false)}
19 | ${this.buildNumberField('Max days', 'max_days', this._config.max_days, null, 1)}
20 | ${this.buildSwitchField('Display classroom', 'display_classroom', this._config.display_classroom, true)}
21 | ${this.buildSwitchField('Display day hours', 'display_day_hours', this._config.display_day_hours, true)}
22 | ${this.buildSwitchField('Display lunch break', 'display_lunch_break', this._config.display_lunch_break, true)}
23 | ${this.buildSwitchField('Dim ended lessons', 'dim_ended_lessons', this._config.dim_ended_lessons, true)}
24 | ${this.buildSwitchField('Enable slider', 'enable_slider', this._config.enable_slider, false)}
25 | ${this.buildSwitchField('Display free time slots', 'display_free_time_slots', this._config.display_free_time_slots, true)}
26 | `;
27 | }
28 | }
29 |
30 | customElements.define("pronote-timetable-card-editor", PronoteTimetableCardEditor);
31 |
--------------------------------------------------------------------------------
/src/pronote.ts:
--------------------------------------------------------------------------------
1 | import './cards/timetable-card.js'
2 | import './cards/homework-card.js'
3 | import './cards/grades-card.js'
4 | import './cards/averages-card.js'
5 | import './cards/evaluations-card.js'
6 | import './cards/absences-card.js'
7 | import './cards/delays-card.js'
8 |
9 | import './editors/timetable-card-editor.js'
10 | import './editors/homework-card-editor.js'
11 | import './editors/grades-card-editor.js'
12 | import './editors/evaluations-card-editor.js'
13 | import './editors/delays-card-editor.js'
14 | import './editors/averages-card-editor.js'
15 | import './editors/absences-card-editor.js'
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "lib": ["es2017", "dom", "dom.iterable"],
7 | "plugins": [
8 | {
9 | "name": "ts-lit-plugin"
10 | }
11 | ],
12 | "noEmit": true,
13 | "noUnusedParameters": true,
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "strict": true,
17 | "noImplicitAny": false,
18 | "skipLibCheck": true,
19 | "resolveJsonModule": true,
20 | "experimentalDecorators": true,
21 | "sourceMap": true,
22 | "allowSyntheticDefaultImports": true
23 | },
24 | "include": ["src/*", "src/**/*"]
25 | }
--------------------------------------------------------------------------------