diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7bb4cf7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..e13b6d4 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,48 @@ +- name: Stale + description: Stale + color: 393C41 +- name: feat + description: A new feature + color: a2eeef +- name: fix + description: A bug fix + color: d3fc03 +- name: docs + description: Documentation only changes + color: D4C5F9 +- name: documentation + description: This issue relates to writing documentation + color: D4C5F9 +- name: doc + description: This issue relates to writing documentation + color: D4C5F9 +- name: style + description: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) + color: D4C5F9 +- name: refactor + description: A code change that neither fixes a bug nor adds a feature + color: D4C5F9 +- name: perf + description: A code change that improves performance + color: d3fc03 +- name: test + description: Adding missing or correcting existing tests + color: d3fc03 +- name: chore + description: Changes to the build process or auxiliary tools and libraries such as documentation generation + color: d3fc03 +- name: major + description: A change requiring a major version bump + color: 6b230e +- name: minor + description: A change requiring a minor version bump + color: cc6749 +- name: patch + description: A change requiring a patch version bump + color: f9d0c4 +- name: breaking + description: A breaking change + color: d30000 +- name: breaking change + description: A breaking change + color: d30000 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..b184f47 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,73 @@ +name-template: "Release v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +template: | + ## What Changed 👀 + + $CHANGES + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION +categories: + - title: 🚀 Features + labels: + - feat + - feature + - enhancement + - title: 🐛 Bug Fixes + labels: + - fix + - bug + - title: ⚠️ Breaking + labels: + - breaking + - breaking change + - title: ⚠️ Changes + labels: + - changed + - title: ⛔️ Deprecated + labels: + - deprecated + - title: 🗑 Removed + labels: + - removed + - title: 🔐 Security + labels: + - security + - title: 📄 Documentation + labels: + - docs + - doc + - documentation + - title: 🛠 Refactoring + labels: + - refactor + - style + - title: 🚀 Performance + labels: + - perf + - title: 🧪 Test + labels: + - test + - title: 👷 Chore + labels: + - chore + - title: 🧩 Dependency Updates + labels: + - deps + - dependencies + collapse-after: 5 + +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - major + minor: + labels: + - minor + patch: + labels: + - patch + default: patch + +exclude-labels: + - skip-changelog diff --git a/.github/scripts/get_new_version.py b/.github/scripts/get_new_version.py new file mode 100644 index 0000000..481e4b7 --- /dev/null +++ b/.github/scripts/get_new_version.py @@ -0,0 +1,69 @@ +"""Script to calculate the next beta version.""" +import os +import sys + +import requests + +# Get repository owner and repository name from the environment variable +repository = os.environ["GITHUB_REPOSITORY"] +owner, repo = repository.split("/") + +# print(f"Repository: {repo}") +# print(f"Owner: {owner}") + +# Get the latest release information +response = requests.get( + f"https://api.github.com/repos/{owner}/{repo}/releases/latest", timeout=10 +) +latest_release = response.json() +latest_version = latest_release["tag_name"] + +ref = os.environ["GITHUB_REF"] + +# Get the commit count since the latest release +response = requests.get( + f"https://api.github.com/repos/{owner}/{repo}/compare/{latest_version}...{ref}", + timeout=10, +) +compare_info = response.json() +commit_count = compare_info["total_commits"] + + +def get_semver_level(commit_messages): + """Extract SemVer level.""" + major_keywords = ["breaking change", "major"] + minor_keywords = ["feat", "minor"] + for message in commit_messages: + if any(keyword in message for keyword in major_keywords): + return "major" + for message in commit_messages: + if any(keyword in message for keyword in minor_keywords): + return "minor" + return "patch" + + +# Determine version components based on commit messages +commit_messages = [] +for commit in compare_info["commits"]: + commit_messages.append(commit["commit"]["message"]) + +bump = get_semver_level(commit_messages) + +major, minor, patch = map(int, latest_version[1:].split(".")) + +if bump == "major": + major += 1 +elif bump == "minor": + minor += 1 +else: + patch += 1 + +# Create the next version +next_version = f"v{major}.{minor}.{patch}" + +# Check if there are any commits since the latest release +if commit_count > 0: + next_version += f"-beta.{commit_count}" + +print(next_version) +sys.exit(0) diff --git a/.github/scripts/pr_extract_labels.py b/.github/scripts/pr_extract_labels.py new file mode 100644 index 0000000..efd097b --- /dev/null +++ b/.github/scripts/pr_extract_labels.py @@ -0,0 +1,43 @@ +"""Script to extract the labels for a PR.""" +import os +import re +import sys + + +def extract_semver_types(commit_messages): + """Extract SemVer types.""" + types = [] + for message in commit_messages: + pattern = r"^(feat|fix|chore|docs|style|refactor|perf|test)(?:\(.+?\))?:\s(.+)$" + match = re.match(pattern, message) + if match and match.group(1) not in types: + types.append(match.group(1)) + return types + + +def get_semver_level(commit_messages): + """Extract SemVer level.""" + major_keywords = ["breaking change", "major"] + minor_keywords = ["feat", "minor"] + for message in commit_messages: + if any(keyword in message for keyword in major_keywords): + return "major" + for message in commit_messages: + if any(keyword in message for keyword in minor_keywords): + return "minor" + return "patch" + + +file_path = "COMMIT_MESSAGES" +if os.path.exists(file_path): + with open(file_path) as file: + messages = [] + for line in file: + messages.append(line.strip()) + semver_level = get_semver_level(messages) + types = extract_semver_types(messages) + types.append(semver_level) + print(types) + sys.exit(0) +else: + sys.exit(f"ERROR: {file_path} does not exist") diff --git a/.github/workflows/bump_version_and_release.yaml b/.github/workflows/bump_version_and_release.yaml new file mode 100644 index 0000000..2a347d9 --- /dev/null +++ b/.github/workflows/bump_version_and_release.yaml @@ -0,0 +1,92 @@ +name: Release - Bump and Release +on: [workflow_dispatch] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + create_release_draft: + name: Create the release draft + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: ⤵️ Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: 🗑 Delete drafts + uses: hugo19941994/delete-draft-releases@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 📝 Draft release + uses: release-drafter/release-drafter@v6 + id: release_drafter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 🔄 Update version in 'VERSION' and push changes + env: + tag_name: ${{ steps.release_drafter.outputs.tag_name }} + GITHUB_REPO: ${{ github.event.repository.name }} + run: | + echo $tag_name > VERSION + + - name: 🚀 Add and commit changes + uses: EndBug/add-and-commit@v9 + with: + message: Bump version ${{ steps.release_drafter.outputs.tag_name }} + + - name: 📝 Publish release + uses: release-drafter/release-drafter@v6 + id: release_published + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + publish: true + + - name: "✏️ Generate release changelog" + uses: heinrichreimer/github-changelog-generator-action@v2.4 + with: + token: ${{ secrets.GH_PAT }} + issues: true + issuesWoLabels: true + pullRequests: true + prWoLabels: true + unreleased: false + addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}' + + - name: ✅ Commit release notes + uses: EndBug/add-and-commit@v9 + with: + message: Commit release notes ${{ steps.release_drafter.outputs.tag_name }} + + - name: 📦 Create zip file + run: | + cd config + zip -r "${{ github.event.repository.name }}.zip" . + mv "${{ github.event.repository.name }}.zip" .. + + - name: 📎 Upload zip file to release + uses: actions/upload-artifact@v4 + with: + name: release-artifact + path: "${{ github.event.repository.name }}.zip" + + - name: 📝 Update release with zip file + run: | + gh release upload ${{ steps.release_drafter.outputs.tag_name }} "${{ github.event.repository.name }}.zip" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 🚀 Discord notification + env: + tag_name: ${{ steps.release_drafter.outputs.tag_name }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + uses: Ilshidur/action-discord@master + with: + args: "New release published: https://github.com/{{ EVENT_PAYLOAD.repository.full_name }}/releases/tag/{{tag_name}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..8ac7aea --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: "Analysis - Dependency Review" +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: "Checkout Repository" + uses: actions/checkout@v4 + - name: "Dependency Review" + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/pr-checker.yml b/.github/workflows/pr-checker.yml new file mode 100644 index 0000000..3bacdbf --- /dev/null +++ b/.github/workflows/pr-checker.yml @@ -0,0 +1,67 @@ +name: PR - Assign and check labels +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + assign_labels: + name: Assign SemVer labels + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: ⤵️ Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get commit messages + id: commit_messages + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + COMMIT_MESSAGES=$(curl -sSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" | \ + jq -r '.[].commit.message') + echo "$COMMIT_MESSAGES" > COMMIT_MESSAGES + echo "$COMMIT_MESSAGES" + + - name: Determine SemVer level + id: semver_level + run: | + labels=$(python .github/scripts/pr_extract_labels.py) + echo Labels: $labels + echo "labels=$labels" >> "$GITHUB_OUTPUT" + + - name: Delete commit messages file + run: | + rm COMMIT_MESSAGES + + - name: Assign SemVer label + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ${{ steps.semver_level.outputs.labels }} + }); + check_semver_labels: + name: Check Semver labels in PR + needs: assign_labels + runs-on: "ubuntu-latest" + steps: + - name: Check for Semver labels + uses: danielchabr/pr-labels-checker@v3.3 + with: + hasSome: major,minor,patch + githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..77832cf --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,17 @@ +name: "PR & Issues - Close stale iteams" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." + close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." + days-before-stale: 30 + days-before-close: 5 + days-before-pr-close: -1 diff --git a/.github/workflows/synchronize-labels.yml b/.github/workflows/synchronize-labels.yml new file mode 100644 index 0000000..81b91c2 --- /dev/null +++ b/.github/workflows/synchronize-labels.yml @@ -0,0 +1,16 @@ +name: Labels - Synchronize +"on": + push: + paths: + - .github/labels.yml + workflow_dispatch: {} +jobs: + synchronize: + name: Synchronize Labels + runs-on: + - ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 8ebf7e6..ca815e8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This repository contains an ESPHome BLE client for interfacing with the Powerdal [![discord](https://img.shields.io/discord/1094331679327408320?style=for-the-badge&logo=discord)](https://discord.gg/PTpExQJsWA) [![MIT License](https://img.shields.io/github/license/geertmeersman/nexxtender?style=flat-square)](https://github.com/geertmeersman/nexxtender/blob/master/LICENSE) +[![github release](https://img.shields.io/github/v/release/geertmeersman/nexxtender?logo=github)](https://github.com/geertmeersman/nexxtender/releases) +[![github release date](https://img.shields.io/github/release-date/geertmeersman/nexxtender)](https://github.com/geertmeersman/nexxtender/releases) [![github last-commit](https://img.shields.io/github/last-commit/geertmeersman/nexxtender)](https://github.com/geertmeersman/nexxtender/commits) [![github contributors](https://img.shields.io/github/contributors/geertmeersman/nexxtender)](https://github.com/geertmeersman/nexxtender/graphs/contributors) [![github commit activity](https://img.shields.io/github/commit-activity/y/geertmeersman/nexxtender?logo=github)](https://github.com/geertmeersman/nexxtender/commits/main) diff --git a/config/nexxtender_packages/generic_data.yaml b/config/nexxtender_packages/generic_data.yaml index 8b87fc2..7806f22 100644 --- a/config/nexxtender_packages/generic_data.yaml +++ b/config/nexxtender_packages/generic_data.yaml @@ -159,8 +159,16 @@ text_sensor: case 0x0003: id(${device_name}_generic_status).publish_state(std::string("Unlocked force eco")); return; + case 0x4001: + id(${device_name}_generic_status).publish_state(std::string("Time Set READY")); + id(script_write_time).execute(); + return; + case 0x4002: + id(${device_name}_generic_status).publish_state(std::string("Time Set SUCCESS")); + return; case 0x4003: - id(${device_name}_generic_status).publish_state(std::string("Time read")); + id(${device_name}_generic_status).publish_state(std::string("Time read POPPED")); + id(script_read_generic_data).execute(); return; case 0x5001: id(${device_name}_generic_status).publish_state(std::string("Config Set READY")); @@ -263,6 +271,32 @@ script: service_uuid: ${ble_uuid_receive_service} characteristic_uuid: ${ble_uuid_generic_command} value: [0x02, 0x40] + - id: script_sync_time + then: + - ble_client.ble_write: + id: ${device_name}_ble_client_id + service_uuid: ${ble_uuid_receive_service} + characteristic_uuid: ${ble_uuid_generic_command} + value: [0x01, 0x40] + - id: script_write_time + then: + - ble_client.ble_write: + id: ${device_name}_ble_client_id + service_uuid: ${ble_uuid_receive_service} + characteristic_uuid: ${ble_uuid_generic_data} + value: !lambda |- + // Get the epoch time integer from ESP32 + std::time_t current_time_epoch = std::time(nullptr); + int epoch_time_int = static_cast(current_time_epoch); + // Convert the integer into a byte array + std::array epoch_time_bytes; + for (size_t i = 0; i < sizeof(epoch_time_int); ++i) { + epoch_time_bytes[i] = (epoch_time_int >> (8 * i)) & 0xFF; + } + // Convert the byte array into a vector + std::vector epoch_time_vector(epoch_time_bytes.begin(), epoch_time_bytes.end()); + return epoch_time_vector; + - id: script_write_config then: - lambda: |- @@ -296,19 +330,37 @@ script: std::string x = id(${device_name}_generic_data).state; logd_x("${device_name}_generic_data", x); if (x.empty()) { return; } - if (!check_crc(x, x.size())) { - ESP_LOGD("${device_name}_generic_data", "Checksum CRC16 Failed"); - return; - } ESP_LOGD("${device_name}_generic_data", "Status is: 0x%x", id(g_${generic_data_id_prefix}_status)); switch(id(g_${generic_data_id_prefix}_status)) { + case 0x4003: + break; case 0x5003: case 0x5006: + if (!check_crc(x, x.size())) { + ESP_LOGD("${device_name}_generic_data", "Checksum CRC16 Failed"); + return; + } break; default: ESP_LOGD("${device_name}_generic_data", "Status not recognized: 0x%x", id(g_${generic_data_id_prefix}_status)); break; } + if (id(g_${generic_data_id_prefix}_status) == 0x4003) { + // Get the epoch time integer from ESP32 + std::time_t current_time_epoch = std::time(nullptr); + int current_time_epoch_int_esp32 = static_cast(current_time_epoch); + // Get the epoch time integer from the BLE + int epoch_time_int = getInt(x, 0); + // Convert epoch time to a human-readable format + std::time_t current_time_epoch_nexxtender = static_cast(epoch_time_int); + char current_time_human_readable[20]; // Adjust size as needed + strftime(current_time_human_readable, sizeof(current_time_human_readable), "%Y-%m-%d %H:%M:%S", localtime(¤t_time_epoch_nexxtender)); + // Log both epoch time and human-readable time + ESP_LOGD("${device_name}_generic_data - time", "Nexxtender Epoch Time: %d, Human Readable Time: %s", epoch_time_int, current_time_human_readable); + strftime(current_time_human_readable, sizeof(current_time_human_readable), "%Y-%m-%d %H:%M:%S", localtime(¤t_time_epoch)); + ESP_LOGD("${device_name}_generic_data - time", " ESP32 Epoch Time: %d, Human Readable Time: %s", current_time_epoch_int_esp32, current_time_human_readable); + return; + } if (id(g_${generic_data_id_prefix}_status) == 0x5006) { //Flag the config read as true size_t index = 0; @@ -405,15 +457,19 @@ script: break; case 12: //touWeekStart(12) id(g_${generic_data_id_prefix}_week_start) = getTimeString(value); + ESP_LOGI("_week_start", "Total minutes: %llu", value); break; case 13: //touWeekStop(13) id(g_${generic_data_id_prefix}_week_end) = getTimeString(value); + ESP_LOGI("_week_end", "Total minutes: %llu", value); break; case 14: //touWeekendStart(14), 0 id(g_${generic_data_id_prefix}_weekend_start) = getTimeString(value); + ESP_LOGI("_weekend_start", "Total minutes: %llu", value); break; case 15: //touWeekendStop(15), 0 id(g_${generic_data_id_prefix}_weekend_end) = getTimeString(value); + ESP_LOGI("_weekend_end", "Total minutes: %llu", value); break; case 16: //timezone(16), 0 break; @@ -494,10 +550,11 @@ script: } else { ESP_LOGW("${device_name}_generic_data", "Config data size not equal to 15. Please open an issue with your HEX string information."); } + // Synchronise Nexxtender time + id(script_sync_time).execute(); return; } - button: - platform: template name: Toggle charger mode @@ -506,8 +563,27 @@ button: then: - lambda: |- id(switch_charger_mode).execute(); + - platform: template + name: Read time + icon: mdi:clock-time-eight-outline + entity_category: diagnostic + on_press: + then: + - lambda: |- + id(script_read_time).execute(); + - platform: template + name: Sync time + icon: mdi:timer-refresh-outline + entity_category: diagnostic + on_press: + - ble_client.ble_write: + id: ${device_name}_ble_client_id + service_uuid: ${ble_uuid_receive_service} + characteristic_uuid: ${ble_uuid_generic_command} + value: [0x01, 0x40] - platform: template name: "Start Charge Default" + icon: mdi:alpha-d-circle-outline on_press: - ble_client.ble_write: id: ${device_name}_ble_client_id @@ -516,6 +592,7 @@ button: value: [0x01, 0x00] - platform: template name: "Start Charge Max" + icon: mdi:alpha-m-circle-outline on_press: - ble_client.ble_write: id: ${device_name}_ble_client_id @@ -524,6 +601,7 @@ button: value: [0x02, 0x00] - platform: template name: "Start Charge Auto" + icon: mdi:alpha-a-circle-outline on_press: - ble_client.ble_write: id: ${device_name}_ble_client_id @@ -532,6 +610,7 @@ button: value: [0x03, 0x00] - platform: template name: "Start Charge Eco" + icon: mdi:alpha-e-circle-outline on_press: - ble_client.ble_write: id: ${device_name}_ble_client_id @@ -540,6 +619,7 @@ button: value: [0x04, 0x00] - platform: template name: "Stop Charging" + icon: mdi:stop-circle-outline on_press: - ble_client.ble_write: id: ${device_name}_ble_client_id @@ -566,21 +646,14 @@ number: std::string config = id(g_${generic_data_id_prefix}_config); if (config.empty()) { return; } float value = static_cast(x); - ESP_LOGD("${device_name}_generic_data", "Maximum car charging speed Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_evse_max)); - if (value > id(g_${generic_data_id_prefix}_i_max)) { - ESP_LOGD("${device_name}_generic_data", "Value ignored since higher than the maximum available capacity, setting it to the max available charging speed"); - ESP_LOGD("${device_name}_generic_data", "Setting Maximum car charging speed to %f A", id(g_${generic_data_id_prefix}_i_max)); - id(g_${generic_data_id_prefix}_i_evse_max) = id(g_${generic_data_id_prefix}_i_max); + ESP_LOGD("Maximum car charging speed", "Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_evse_max)); + if (id(g_${generic_data_id_prefix}_i_evse_max) == 0) { + ESP_LOGD("Setting Maximum car charging speed", "%f A", value); + id(g_${generic_data_id_prefix}_i_evse_max) = value; + } else if (id(g_${generic_data_id_prefix}_i_evse_max) != value) { + ESP_LOGD("Setting Maximum car charging speed and sending config", "Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_evse_max)); + id(g_${generic_data_id_prefix}_i_evse_max) = value; id(script_send_config_old).execute(); - } else { - if (id(g_${generic_data_id_prefix}_i_evse_max) == 0) { - ESP_LOGD("${device_name}_generic_data", "Setting Maximum car charging speed to %f A", value); - id(g_${generic_data_id_prefix}_i_evse_max) = value; - } else if (id(g_${generic_data_id_prefix}_i_evse_max) != value) { - ESP_LOGD("${device_name}_generic_data", "Setting Maximum car charging speed and sending config, Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_evse_max)); - id(g_${generic_data_id_prefix}_i_evse_max) = value; - id(script_send_config_old).execute(); - } } - platform: template id: max_available_capacity @@ -600,15 +673,12 @@ number: std::string config = id(g_${generic_data_id_prefix}_config); if (config.empty()) { return; } float value = static_cast(x); - ESP_LOGD("${device_name}_generic_data", "Maximum available capacity: Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_max)); - if (value < id(g_${generic_data_id_prefix}_i_evse_max)) { - id(g_${generic_data_id_prefix}_i_evse_max) = value; - } + ESP_LOGD("Maximum available capacity", "Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_max)); if (id(g_${generic_data_id_prefix}_i_max) == 0) { - ESP_LOGD("${device_name}_generic_data", "Setting Maximum available capacity: %f A", value); + ESP_LOGD("Setting Maximum available capacity", "%f A", value); id(g_${generic_data_id_prefix}_i_max) = value; } else if (id(g_${generic_data_id_prefix}_i_max) != value) { - ESP_LOGD("${device_name}_generic_data", "Setting Maximum available capacity and sending config: Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_max)); + ESP_LOGD("Setting Maximum available capacity and sending config", "Received: %f, previous value: %f", value, id(g_${generic_data_id_prefix}_i_max)); id(g_${generic_data_id_prefix}_i_max) = value; id(script_send_config_old).execute(); } @@ -672,3 +742,18 @@ binary_sensor: - lambda: |- ESP_LOGD("Boot switch", "Released"); id(switch_charger_mode).execute(); + +# Services in API +api: + services: + - service: set_week_schema_start + variables: + time_string: string + then: + - lambda: |- + int total_minutes = convert_time_string_to_minutes(time_string); + if (total_minutes < 0) { + // An error occurred + return; + } + ESP_LOGI("set_week_schema_start", "Total minutes: %d", total_minutes); diff --git a/config/nexxtender_packages/nexxtender.h b/config/nexxtender_packages/nexxtender.h index 84f5e5a..c39db6a 100644 --- a/config/nexxtender_packages/nexxtender.h +++ b/config/nexxtender_packages/nexxtender.h @@ -79,4 +79,36 @@ void logd_s(const char* device_name, String x) { ESP_LOGD(device_name, "%s", x.c_str()); } +int convert_time_string_to_minutes(const std::string &time_str) { + // Parse the input string + int hours, minutes; + if (sscanf(time_str.c_str(), "%2dh%2d", &hours, &minutes) != 2) { + ESP_LOGE("convert_time_string_to_minutes", "Invalid time format: %s", time_str.c_str()); + return -1; // Return an error value + } + + // Verify hours and minutes are within valid range + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + ESP_LOGE("convert_time_string_to_minutes", "Invalid time range: %02d:%02d", hours, minutes); + return -1; // Return an error value + } + + // Calculate total minutes + int total_minutes = hours * 60 + minutes; + return total_minutes; +} + +// Function to check if daylight saving time (DST) is in effect +bool is_dst(int timestamp_utc) { + // DST starts on the last Sunday of March and ends on the last Sunday of October + // Determine the year of the timestamp + int year = timestamp_utc / (365 * 24 * 60); // Assuming a non-leap year + // Calculate the last Sunday in March + int last_sunday_march = 31 - ((5 * year / 4 + 4) % 7); // Daylight saving time starts + // Calculate the last Sunday in October + int last_sunday_october = 31 - ((5 * (year + 1) / 4 + 1) % 7); // Daylight saving time ends + // Check if the timestamp falls within the DST period + return (timestamp_utc >= last_sunday_march * 24 * 60 && timestamp_utc < last_sunday_october * 24 * 60); +} + #endif diff --git a/config/nexxtender_packages/time.yaml b/config/nexxtender_packages/time.yaml index a63b357..d91fcb4 100644 --- a/config/nexxtender_packages/time.yaml +++ b/config/nexxtender_packages/time.yaml @@ -1,11 +1,13 @@ time: - - platform: sntp - id: sntp_time - timezone: Europe/Brussels - servers: - - 0.at.pool.ntp.org - - 0.pool.ntp.org - - 1.pool.ntp.org +# - platform: sntp +# id: sntp_time +# timezone: Europe/Brussels +# servers: +# - 0.at.pool.ntp.org +# - 0.pool.ntp.org +# - 1.pool.ntp.org + - platform: homeassistant + id: homeassistant_time on_time_sync: then: - - logger.log: "Synchronized sntp clock" + - logger.log: "Synchronized system clock with HA"