├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── feature_request.yml │ └── general_question.yml ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── build-image.yaml │ ├── lint.yaml │ └── test.yaml ├── .gitignore ├── .run ├── App image.run.xml └── Testing image.run.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── ReadMe.md ├── buf.gen.yaml ├── buf.yaml ├── build └── Dockerfile ├── cmd ├── export.go ├── import.go ├── root.go ├── run.go └── version.go ├── deployments └── docker │ └── docker-compose.yaml ├── docs ├── configuration │ ├── configuration.md │ ├── evse-configuration.md │ ├── flags.md │ ├── importing-and-exporting.md │ └── ocpp │ │ ├── data-transfer-payloads.md │ │ ├── ocpp-16.md │ │ └── ocpp-201.md ├── contribution │ ├── client │ │ ├── ChargePi-overview.png │ │ ├── api.md │ │ └── internal-structure.md │ ├── hardware │ │ ├── adding-support-for-hardware.md │ │ ├── display │ │ │ ├── display-contrib.md │ │ │ └── i18n.md │ │ ├── evcc │ │ │ └── evcc-contrib.md │ │ ├── indicator │ │ │ └── indicator-contrib.md │ │ ├── powerMeter │ │ │ └── power-meter-contrib.md │ │ └── reader │ │ │ └── reader-contrib.md │ └── ui │ │ └── ui.md ├── getting-started │ ├── README.md │ ├── guides │ │ └── assembly │ │ │ ├── WiringSketch_eng.png │ │ │ └── bill-of-materials.md │ └── installation │ │ ├── libraries.md │ │ ├── running-the-client.md │ │ └── service-setup.md └── hardware │ ├── display │ └── hd44.md │ ├── evcc │ └── relay.md │ ├── hardware-support.md │ ├── indicator │ └── ws281x.md │ ├── misc │ └── 4g-modem │ │ └── modem-setup.md │ ├── power-meter │ └── cs5460a.md │ └── readers │ └── pn532.md ├── gen └── proto │ ├── charge_point │ └── v1 │ │ ├── charge_point.pb.go │ │ └── charge_point_grpc.pb.go │ ├── common │ └── v1 │ │ ├── charge_point.pb.go │ │ ├── connector.pb.go │ │ ├── hardware.pb.go │ │ └── session.pb.go │ ├── configuration │ └── v1 │ │ ├── display.pb.go │ │ ├── display_grpc.pb.go │ │ ├── indicator.pb.go │ │ ├── indicator_grpc.pb.go │ │ ├── ocpp.pb.go │ │ ├── ocpp_grpc.pb.go │ │ ├── tag_reader.pb.go │ │ └── tag_reader_grpc.pb.go │ ├── connection │ └── v1 │ │ ├── connection.pb.go │ │ └── connection_grpc.pb.go │ ├── evse │ └── v1 │ │ ├── evcc.pb.go │ │ ├── evse.pb.go │ │ ├── evse_grpc.pb.go │ │ └── power_meter.pb.go │ ├── logs │ └── v1 │ │ ├── log.pb.go │ │ └── log_grpc.pb.go │ ├── tags │ └── v1 │ │ ├── tags.pb.go │ │ └── tags_grpc.pb.go │ └── users │ └── v1 │ ├── users.pb.go │ └── users_grpc.pb.go ├── go.mod ├── go.sum ├── internal ├── api │ ├── grpc │ │ ├── auth.go │ │ ├── charge_point.go │ │ ├── configuration.go │ │ ├── connectivity.go │ │ ├── evse.go │ │ ├── logs.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── user.go │ └── http │ │ ├── application.go │ │ └── ui-server.go ├── auth │ ├── cache │ │ ├── auth-cache.go │ │ └── auth-cache_test.go │ ├── contst_test.go │ ├── list │ │ ├── local-auth-list.go │ │ └── local-auth-list_test.go │ ├── manager.go │ └── manager_test.go ├── chargepoint │ ├── api.go │ ├── charge-point.go │ ├── main.go │ └── v16 │ │ ├── boot-notification.go │ │ ├── charge-point-details.go │ │ ├── charge-point.go │ │ ├── charge-point_test.go │ │ ├── connector-functions.go │ │ ├── connector-functions_test.go │ │ ├── core-profile.go │ │ ├── core-profile_test.go │ │ ├── firmware-updates.go │ │ ├── hardware.go │ │ ├── hardware_test.go │ │ ├── helpers.go │ │ ├── localauth.go │ │ ├── reservations.go │ │ ├── reservations_test.go │ │ ├── setters.go │ │ ├── start-charging.go │ │ ├── stop-charging.go │ │ ├── tag-auth.go │ │ ├── trigger-message.go │ │ └── trigger-message_test.go ├── diagnostics │ └── manager.go ├── evse │ ├── evcc.go │ ├── evse.go │ ├── evse_test.go │ ├── manager │ │ ├── manager-reservations.go │ │ ├── manager.go │ │ └── manager_test.go │ ├── power-meter.go │ └── status.go ├── pkg │ ├── configuration │ │ └── configuration.go │ ├── database │ │ ├── database.go │ │ ├── keys.go │ │ ├── logger.go │ │ └── migration.go │ ├── models │ │ ├── charge-point │ │ │ ├── consts.go │ │ │ ├── model.go │ │ │ └── opts.go │ │ ├── notifications │ │ │ ├── meter-values.go │ │ │ └── status.go │ │ └── settings │ │ │ ├── auth.go │ │ │ ├── charge-point.go │ │ │ ├── common.go │ │ │ ├── consts.go │ │ │ ├── evse.go │ │ │ └── ocpp.go │ ├── scheduler │ │ └── scheduler.go │ ├── settings │ │ ├── exporter.go │ │ ├── exporter_test.go │ │ ├── importer.go │ │ ├── importer_test.go │ │ ├── manager.go │ │ └── manager_test.go │ └── util │ │ ├── helpers.go │ │ └── ocpp.go ├── sessions │ ├── pkg │ │ ├── database │ │ │ ├── session-badger.go │ │ │ ├── session-badger_test.go │ │ │ └── session.go │ │ └── models │ │ │ ├── session.go │ │ │ └── session_test.go │ └── service │ │ └── session │ │ ├── session.go │ │ └── session_test.go ├── smart-charging │ ├── composite-schedule.go │ ├── helpers.go │ ├── helpers_test.go │ ├── manager.go │ └── manager_test.go └── users │ ├── pkg │ ├── database │ │ ├── badger.go │ │ ├── badger_test.go │ │ └── interface.go │ └── models │ │ └── user.go │ └── service │ ├── service.go │ └── service_test.go ├── main.go ├── makefile ├── pkg ├── display │ ├── display.go │ ├── dummy.go │ ├── hd44780.go │ └── i18n │ │ ├── i18n.go │ │ ├── i18n_test.go │ │ ├── messages.go │ │ └── translations │ │ ├── active.en.yaml │ │ └── active.sl.yaml ├── evcc │ ├── dummy.go │ ├── evcc.go │ ├── relay.go │ └── states.go ├── indicator │ ├── dummy.go │ ├── indicator.go │ └── ws281x.go ├── models │ ├── ocpp │ │ ├── data-transfer-charge-point.go │ │ ├── data-transfer-connector.go │ │ └── version.go │ └── settings │ │ ├── display.go │ │ ├── evcc.go │ │ ├── hardware-interfaces.go │ │ ├── indicator.go │ │ ├── power-meter.go │ │ └── reader.go ├── observability │ └── logging │ │ ├── formats.go │ │ └── logging.go ├── power-meter │ ├── cs5460a.go │ ├── dummy.go │ └── power-meter.go ├── reader │ ├── TagReader.go │ ├── TagReader_linux.go │ ├── dummy.go │ └── reader.go └── util │ └── random-tag-id-generator.go ├── proto ├── charge_point │ └── v1 │ │ └── charge_point.proto ├── common │ └── v1 │ │ ├── charge_point.proto │ │ ├── connector.proto │ │ ├── hardware.proto │ │ └── session.proto ├── configuration │ └── v1 │ │ ├── display.proto │ │ ├── indicator.proto │ │ ├── ocpp.proto │ │ └── tag_reader.proto ├── connection │ └── v1 │ │ └── connection.proto ├── evse │ └── v1 │ │ ├── evcc.proto │ │ ├── evse.proto │ │ └── power_meter.proto ├── logs │ └── v1 │ │ └── log.proto ├── tags │ └── v1 │ │ └── tags.proto └── users │ └── v1 │ └── users.proto ├── scripts ├── certificates │ ├── create-test-certs.sh │ └── openssl-cp.conf └── install-dependencies.sh └── ui ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── modals │ │ └── DeleteModal.svelte │ ├── navigation │ │ └── NavItem.svelte │ └── theme │ │ └── ThemeToggle.svelte └── routes │ ├── (auth) │ └── login │ │ └── +page.svelte │ ├── (dashboard) │ ├── +layout.svelte │ ├── +page.svelte │ ├── evse │ │ └── +page.svelte │ ├── hardware │ │ └── +page.svelte │ ├── logs │ │ └── +page.svelte │ ├── ocpp │ │ └── +page.svelte │ └── users │ │ ├── +page.svelte │ │ └── components │ │ ├── CreateUserForm.svelte │ │ └── DeleteUserButton.svelte │ └── +layout.svelte ├── static ├── favicon.png └── logo.svg ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | 9 | LICENSE.txt 10 | .idea 11 | .github/ 12 | build/ 13 | docs/ 14 | deployments/ 15 | ReadMe.md -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @xBlaz3kx 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug Report' 2 | description: 'Submit a bug report' 3 | title: '🐛 Bug: ' 4 | labels: [ 'type: bug' ] 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: '📜 Description' 12 | description: 'A clear and concise description of what the bug is.' 13 | placeholder: 'It bugs out when ...' 14 | - type: textarea 15 | id: steps-to-reproduce 16 | validations: 17 | required: true 18 | attributes: 19 | label: '👟 Reproduction steps' 20 | description: 'How do you trigger this bug? Please walk us through it step by step.' 21 | placeholder: "Charge point sends a BootNotification request to the Central System." 22 | - type: textarea 23 | id: expected-behavior 24 | validations: 25 | required: true 26 | attributes: 27 | label: '👍 Expected behavior' 28 | description: 'What did you think should happen?' 29 | placeholder: 'It should ...' 30 | - type: textarea 31 | id: actual-behavior 32 | validations: 33 | required: true 34 | attributes: 35 | label: '👎 Actual Behavior' 36 | description: 'What did actually happen? Add screenshots, if applicable.' 37 | placeholder: 'It actually ...' 38 | - type: checkboxes 39 | id: ocpp-version 40 | attributes: 41 | label: 'What OCPP version are you using?' 42 | options: 43 | - label: "OCPP 1.6" 44 | required: false 45 | - label: "OCPP 2.0.1" 46 | required: false 47 | - type: checkboxes 48 | id: ocpp-extensions 49 | attributes: 50 | label: 'Are you using any OCPP extensions?' 51 | options: 52 | - label: "OCPP 1.6 Security" 53 | required: false 54 | - label: "OCPP 1.6 Plug and Charge" 55 | required: false 56 | - type: input 57 | id: release-version 58 | validations: 59 | required: false 60 | attributes: 61 | label: release version 62 | description: Mention the release version of the software you are using. 63 | placeholder: v1.2.3 64 | - type: textarea 65 | id: additional-context 66 | validations: 67 | required: false 68 | attributes: 69 | label: '📃 Provide any additional context for the Bug.' 70 | description: 'Add any other context about the problem here.' 71 | placeholder: 'It actually ...' 72 | - type: checkboxes 73 | id: no-duplicate-issues 74 | attributes: 75 | label: '👀 Have you spent some time to check if this bug has been found before?' 76 | options: 77 | - label: "I checked and didn't find a similar issue" 78 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature 2 | description: 'Submit a proposal for a new feature' 3 | title: '🚀 Feature: ' 4 | labels: [ feature ] 5 | body: 6 | - type: textarea 7 | id: feature-description 8 | validations: 9 | required: true 10 | attributes: 11 | label: '🔖 Feature description' 12 | description: 'A clear and concise description of what the feature is.' 13 | placeholder: 'You should add ...' 14 | - type: textarea 15 | id: pitch 16 | validations: 17 | required: true 18 | attributes: 19 | label: '🎤 Why is this feature needed ?' 20 | description: 'Please explain why this feature should be implemented and how it would be used. Add examples, if 21 | applicable.' 22 | placeholder: 'In my use-case, ...' 23 | - type: textarea 24 | id: solution 25 | validations: 26 | required: true 27 | attributes: 28 | label: '✌️ How do you aim to achieve this?' 29 | description: 'A clear and concise description of what you want to happen.' 30 | placeholder: 'I want this feature to, ...' 31 | - type: textarea 32 | id: alternative 33 | validations: 34 | required: false 35 | attributes: 36 | label: '🔄️ Additional Information' 37 | description: "A clear and concise description of any alternative solutions or additional solutions you've considered." 38 | placeholder: 'I tried, ...' 39 | - type: checkboxes 40 | id: no-duplicate-issues 41 | attributes: 42 | label: '👀 Have you spent some time to check if this feature request has been raised before?' 43 | options: 44 | - label: "I checked and didn't find similar issue" 45 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.yml: -------------------------------------------------------------------------------- 1 | name: '❔ Question' 2 | description: 'Submit a general question to the community.' 3 | title: '❔ Question: ' 4 | labels: [ 'type: question' ] 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: '❔ What is your question?' 12 | description: 'The stage is yours. Ask away! Try to provide as much context as possible.' 13 | placeholder: 'What is the best way to ...' 14 | - type: checkboxes 15 | id: ocpp-version 16 | attributes: 17 | label: 'Which OCPP version referring to?' 18 | options: 19 | - label: "OCPP 1.6" 20 | required: false 21 | - label: "OCPP 2.0.1" 22 | required: false 23 | - type: checkboxes 24 | id: ocpp-extensions 25 | attributes: 26 | label: 'Are you using any OCPP extensions?' 27 | options: 28 | - label: "OCPP 1.6 Security" 29 | required: false 30 | - label: "OCPP 1.6 Plug and Charge" 31 | required: false 32 | - type: checkboxes 33 | id: no-duplicate-issues 34 | attributes: 35 | label: '👀 Have you spent some time to check if this question has been asked before?' 36 | options: 37 | - label: "I checked and didn't find a similar issue" 38 | required: true -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | assignees: 8 | - "xBlaz3kx" 9 | reviewers: 10 | - "xBlaz3kx" 11 | 12 | - package-ecosystem: "docker" 13 | directory: "/build/package/" 14 | schedule: 15 | interval: "weekly" 16 | assignees: 17 | - "xBlaz3kx" 18 | reviewers: 19 | - "xBlaz3kx" 20 | 21 | - package-ecosystem: "gomod" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | assignees: 26 | - "xBlaz3kx" 27 | reviewers: 28 | - "xBlaz3kx" -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. 4 | If it fixes a bug or resolves a feature request, be sure to link to that issue. 5 | 6 | ## Types of changes 7 | 8 | What types of changes does your code introduce? 9 | _Put an `x` in the boxes that apply_ 10 | 11 | - [ ] Bugfix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Documentation Update (if none of the other choices apply) 15 | 16 | ## Checklist 17 | 18 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of 19 | them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before 20 | merging your code._ 21 | 22 | - [ ] I have read the [CONTRIBUTING](https://github.com/ChargePi/ChargePi-go/blob/master/CONTRIBUTING.md) doc 23 | - [ ] I have signed the CLA 24 | - [ ] Lint and unit tests pass locally with my changes 25 | - [ ] I have added tests that prove my fix is effective or that my feature works 26 | - [ ] I have added necessary documentation (if appropriate) 27 | - [ ] Any dependent changes have been merged and published in downstream modules 28 | 29 | ## Further comments 30 | 31 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you 32 | did and what alternatives you considered, etc... -------------------------------------------------------------------------------- /.github/workflows/build-image.yaml: -------------------------------------------------------------------------------- 1 | name: "Build Docker Image" 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | paths-ignore: 7 | - '.run/**' 8 | - 'docs/**' 9 | - 'deployments/**' 10 | - 'scripts/**' 11 | - '*.md' 12 | branches: 13 | - main 14 | - v2 15 | push: 16 | paths-ignore: 17 | - '.run/**' 18 | - 'docs/**' 19 | - 'deployments/**' 20 | - 'scripts/**' 21 | - '*.md' 22 | branches: 23 | - main 24 | - v2 25 | 26 | jobs: 27 | build: 28 | name: Build a Docker image 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Set up Docker Buildx 39 | id: buildx 40 | uses: docker/setup-buildx-action@v3 41 | 42 | - name: Cache Docker layers 43 | uses: actions/cache@v4 44 | with: 45 | path: go-build-cache 46 | key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} 47 | 48 | - name: inject go-build-cache into docker 49 | # v1 was composed of two actions: "inject" and "extract". 50 | # v2 is unified to a single action. 51 | uses: reproducible-containers/buildkit-cache-dance@v3.2.0 52 | with: 53 | cache-source: go-build-cache 54 | 55 | - name: Login to Docker Hub 56 | uses: docker/login-action@v3 57 | with: 58 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 59 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 60 | 61 | - name: Generate Docker metadata 62 | id: meta 63 | uses: docker/metadata-action@v5 64 | with: 65 | images: | 66 | xblaz3kx/chargepi 67 | tags: | 68 | type=ref,event=branch 69 | type=semver,pattern={{version}} 70 | type=semver,pattern={{major}}.{{minor}} 71 | type=sha,event=pr 72 | flavor: | 73 | latest=true 74 | 75 | - name: Build and push 76 | uses: docker/build-push-action@v6 77 | with: 78 | context: . 79 | target: chargepi 80 | build-args: | 81 | READER_CONNECTION_TYPE="pn532_uart" 82 | file: ./build/Dockerfile 83 | platforms: linux/arm64 84 | push: ${{ github.event_name == 'push' }} 85 | tags: ${{ steps.meta.outputs.tags }} -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: "Lint" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - v2 8 | paths-ignore: 9 | - '.run/**' 10 | - 'docs/**' 11 | - 'deployments/**' 12 | - '*.md' 13 | 14 | pull_request: 15 | branches: 16 | - main 17 | - v2 18 | types: [ opened, synchronize ] 19 | paths-ignore: 20 | - '.run/**' 21 | - 'docs/**' 22 | - 'deployments/**' 23 | - '*.md' 24 | 25 | jobs: 26 | lint: 27 | name: Lint Go code 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | 34 | - uses: actions/setup-go@v5 35 | with: 36 | go-version: 1.23.0 37 | 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@v7 40 | with: 41 | version: v2.0 42 | args: --timeout=3m 43 | 44 | buf: 45 | name: Run buf 46 | runs-on: ubuntu-latest 47 | permissions: 48 | contents: read 49 | pull-requests: write 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v4 53 | 54 | - uses: bufbuild/buf-action@v1 55 | with: 56 | github_token: ${{ github.token }} -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Test and lint" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - v2 8 | paths-ignore: 9 | - '.run/**' 10 | - 'docs/**' 11 | - 'deployments/**' 12 | - 'scripts/**' 13 | - '*.md' 14 | 15 | pull_request: 16 | branches: 17 | - main 18 | - v2 19 | types: [ opened, synchronize ] 20 | paths-ignore: 21 | - '.run/**' 22 | - 'docs/**' 23 | - 'deployments/**' 24 | - 'scripts/**' 25 | - '*.md' 26 | 27 | jobs: 28 | # Run unit tests 29 | unit: 30 | name: "Run unit tests" 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: 1.23.0 40 | 41 | # todo skip mocks coverage 42 | - name: Install dependencies and run tests 43 | run: | 44 | go mod download 45 | go test -v ./... -coverpkg=./... -short -coverprofile=unit_coverage.out 46 | 47 | - name: Archive code coverage results 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: code-coverage 51 | path: unit_coverage.out 52 | 53 | integration: 54 | name: "Run integration tests" 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: Setup Go 61 | uses: actions/setup-go@v5 62 | with: 63 | go-version: 1.23.0 64 | 65 | - name: Install dependencies and run tests 66 | run: | 67 | go mod download 68 | go test -v -run 'Integration$' ./... -coverpkg=./... -coverprofile=integration_coverage.out 69 | 70 | - name: Archive code coverage results 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: integration-coverage 74 | path: integration_coverage.out 75 | 76 | code_coverage: 77 | name: "Code coverage report" 78 | if: github.event_name == 'pull_request' # Do not run when workflow is triggered by push to main branch 79 | runs-on: ubuntu-latest 80 | needs: [ unit, integration ] 81 | continue-on-error: true # not critical 82 | permissions: 83 | contents: read 84 | actions: read 85 | pull-requests: write # write permission needed to comment on PR 86 | steps: 87 | - uses: fgrosse/go-coverage-report@v1.2.0 88 | with: 89 | coverage-artifact-name: code-coverage 90 | coverage-file-name: unit_coverage.out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | 9 | .idea 10 | configs 11 | !configs/casbin/* 12 | .DS_Store 13 | rpi_ws281x 14 | ui/.next 15 | ui/build -------------------------------------------------------------------------------- /.run/App image.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.run/Testing image.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ChargePi-go 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Adding support for new hardware 10 | - Becoming a maintainer 11 | 12 | ## Naming conventions 13 | 14 | Be sure to follow the naming conventions used throughout the project (e.g., variable names, function names, file names, 15 | etc.). 16 | 17 | Also make sure the commit messages are clear and concise, following 18 | the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 19 | 20 | ## Reporting issues 21 | 22 | If you find a bug in the project, please open an issue on the project's GitHub repository. When opening an issue, please 23 | provide as much information as possible, including the version of the project you are using, the operating system you 24 | are using, and any relevant error messages and logs. 25 | 26 | ### New features 27 | 28 | If you would like to contribute code to the project, please follow these steps: 29 | 30 | 1. Fork the project on GitHub. 31 | 2. Create a new branch for your changes. 32 | 3. Make your changes on the new branch. 33 | 4. Write tests for your changes. 34 | 5. Run the tests to make sure they pass. 35 | 6. Submit a pull request to the project's GitHub repository. 36 | 7. Create necessary documentation for your changes, for example, how to use the new feature, how to configure it and 37 | preferably design docs. 38 | 39 | That's it! We will review your pull request and provide feedback as soon as possible. 40 | 41 | ### Hardware support 42 | 43 | If you would like to add support for new hardware to the project, please follow these steps: 44 | 45 | 1. Fork the project on GitHub. 46 | 2. Create a new branch for your changes. 47 | 3. Make your changes on the new branch. 48 | 4. Write tests for your changes. 49 | 5. Run the tests to make sure they pass and perform necessary hardware tests. 50 | 6. Submit a pull request to the project's GitHub repository. 51 | 7. Create documentation for the new hardware, describing features, installation and usage. 52 | 8. (Optional) Write some guidelines on how to use, handle and maintain the new hardware. 53 | 54 | For more details, check out 55 | the [hardware support guidelines](./docs/contribution/hardware/adding-support-for-hardware.md). 56 | 57 | ## Any contributions you make will be under the MIT Software License 58 | 59 | In short, when you submit code changes, your submissions are understood to be under the 60 | same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the 61 | maintainers if that's a concern. 62 | 63 | ## References 64 | 65 | This document was adapted from the open-source contribution guidelines 66 | for [Facebook's Draft](https://github.com/facebook/draft-js) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 The ChargePi project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | disable: 4 | # Don't modify any file option or field option for googleapis 5 | - module: buf.build/googleapis/googleapis 6 | enabled: true 7 | override: 8 | - file_option: go_package_prefix 9 | value: github.com/ChargePi/ChargePi-go/gen/proto 10 | 11 | inputs: 12 | - directory: ./proto 13 | plugins: 14 | #- remote: buf.build/protocolbuffers/go:v1.31.0 15 | - local: protoc-gen-go 16 | out: gen/proto 17 | opt: 18 | - paths=source_relative 19 | #- remote: buf.build/grpc/go:v1.5.1 20 | - local: protoc-gen-go-grpc 21 | out: gen/proto 22 | opt: 23 | - paths=source_relative 24 | 25 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | breaking: 3 | ignore: 4 | - "*.proto" 5 | modules: 6 | - path: proto 7 | lint: 8 | use: 9 | - DEFAULT 10 | - FILE_LOWER_SNAKE_CASE 11 | - PACKAGE_NO_IMPORT_CYCLE 12 | - PACKAGE_SAME_GO_PACKAGE 13 | - FIELD_LOWER_SNAKE_CASE 14 | - ENUM_VALUE_UPPER_SNAKE_CASE 15 | - PACKAGE_LOWER_SNAKE_CASE 16 | except: 17 | - FIELD_NOT_REQUIRED 18 | enum_zero_value_suffix: _UNSPECIFIED 19 | rpc_allow_same_request_response: false 20 | rpc_allow_google_protobuf_empty_requests: true 21 | rpc_allow_google_protobuf_empty_responses: true 22 | service_suffix: Service -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.23.0 AS base 2 | 3 | WORKDIR /ChargePi/src 4 | COPY . /ChargePi/src 5 | 6 | ARG READER_TYPE 7 | ENV READER_TYPE=$READER_TYPE 8 | 9 | ENV GOCACHE=/root/.cache/go-build 10 | ENV GOMODCACHE=/root/.cache/go-build 11 | ENV GO111MODULE=on 12 | 13 | # Install dependencies 14 | RUN chmod +x /ChargePi/src/scripts/install-dependencies.sh 15 | RUN /ChargePi/src/scripts/install-dependencies.sh $READER_TYPE 0 16 | 17 | ENV GOOS=linux 18 | ENV GOARCH=arm64 19 | ENV CGO=1 20 | 21 | # Compile the client 22 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download 23 | RUN go mod verify 24 | RUN --mount=type=cache,target="/root/.cache/go-build" go build -o ChargePi . 25 | 26 | FROM node:19 as build-ui 27 | 28 | WORKDIR /ui 29 | 30 | COPY ./ui/package*.json . 31 | COPY ./ui . 32 | 33 | RUN --mount=type=cache,target=/root/.npm npm install 34 | RUN npm run build 35 | 36 | # Test the client 37 | FROM --platform=$BUILDPLATFORM vektra/mockery:v2.9.4 AS test 38 | 39 | COPY --from=base /ChargePi/src /ChargePi/src 40 | WORKDIR /ChargePi/src 41 | 42 | RUN cp -r configs/ test/ \ 43 | && cd test/ \ 44 | && chmod +x create-test-certs.sh \ 45 | && ./create-test-certs.sh 46 | 47 | # Gen mocks 48 | RUN mockery 49 | 50 | # Run tests 51 | CMD ["go", "test","-v","./..."] 52 | 53 | FROM --platform=$BUILDPLATFORM alpine AS chargepi 54 | 55 | WORKDIR /etc/ChargePi 56 | 57 | ARG READER_TYPE 58 | ENV READER_TYPE=$READER_TYPE 59 | 60 | COPY --from=base /ChargePi/src/scripts /etc/ChargePi/scripts 61 | 62 | # Install dependencies 63 | RUN chmod +x ./scripts/install-dependencies.sh 64 | RUN ./scripts/install-dependencies.sh $READER_TYPE 0 65 | 66 | # Copy the executable 67 | COPY --from=base /ChargePi/src/configs /etc/ChargePi/configs 68 | COPY --from=base /ChargePi/src/ChargePi /usr/bin/ChargePi 69 | 70 | # Copy the UI static files 71 | COPY --from=build-ui /ui/build/ /usr/bin/ChargePi/ui/build 72 | 73 | EXPOSE 8080 74 | HEALTHCHECK --interval=5s --timeout=3s CMD curl --fail http://localhost:8081/healthz || exit 1 75 | 76 | ENTRYPOINT ["ChargePi"] 77 | CMD ["-c", "/etc/ChargePi/configs/config.yaml", "run"] -------------------------------------------------------------------------------- /cmd/export.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 7 | cfg "github.com/ChargePi/ChargePi-go/internal/pkg/settings" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | exportEvseFolderPath *string 13 | exportOcppConfigurationFilePath *string 14 | exportAuthFilePath *string 15 | exportSettingsFilePath *string 16 | ) 17 | 18 | // exportCommand represents the export command 19 | func exportCommand() *cobra.Command { 20 | exportCmd := &cobra.Command{ 21 | Use: "export", 22 | Short: "Export settings from ChargePi.", 23 | Long: ``, 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | exporter := cfg.GetExporter() 26 | 27 | evseFlag := cmd.Flags().Lookup(settings.EvseFlag).Changed 28 | ocppFlag := cmd.Flags().Lookup(settings.OcppConfigPathFlag).Changed 29 | authFlag := cmd.Flags().Lookup(settings.AuthFileFlag).Changed 30 | settingsFlag := cmd.Flags().Lookup(settings.SettingsFlag).Changed 31 | 32 | // If the flag was set, export the EVSE configurations 33 | if evseFlag { 34 | err := exporter.ExportEVSESettingsToFile(*exportEvseFolderPath) 35 | if err != nil { 36 | return fmt.Errorf("could not export EVSE settings: %v", err) 37 | } 38 | } 39 | 40 | // If the flag was set, export the OCPP configuration 41 | if ocppFlag { 42 | err := exporter.ExportOcppConfigurationToFile(*exportOcppConfigurationFilePath) 43 | if err != nil { 44 | return fmt.Errorf("could not export OCPP configuration: %v", err) 45 | } 46 | } 47 | 48 | // If the flag was set, export tags. 49 | if authFlag { 50 | err := exporter.ExportLocalAuthListToFile(*exportAuthFilePath) 51 | if err != nil { 52 | return fmt.Errorf("could not export tags: %v", err) 53 | } 54 | } 55 | 56 | // If the flag was set, export settings. 57 | if settingsFlag { 58 | err := exporter.ExportChargePointSettingsToFile(*exportSettingsFilePath) 59 | if err != nil { 60 | return fmt.Errorf("could not export settings: %v", err) 61 | } 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | 68 | exportEvseFolderPath = exportCmd.Flags().String(settings.EvseFlag, "./configs/evses", "evse folder path") 69 | exportOcppConfigurationFilePath = exportCmd.Flags().String(settings.OcppConfigPathFlag, "./configs/ocpp.yaml", "OCPP config file path") 70 | exportAuthFilePath = exportCmd.Flags().String(settings.AuthFileFlag, "./configs/authorization.yaml", "authorization file path") 71 | exportSettingsFilePath = exportCmd.Flags().String(settings.SettingsFlag, "./configs/settings.yaml", "settings file path") 72 | 73 | return exportCmd 74 | } 75 | -------------------------------------------------------------------------------- /cmd/import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 7 | cfg "github.com/ChargePi/ChargePi-go/internal/pkg/settings" 8 | "github.com/ChargePi/ChargePi-go/pkg/models/ocpp" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | evseFolderPath *string 14 | ocppConfigurationFilePath *string 15 | ocppVersionFlag *string 16 | authFilePath *string 17 | importSettingsFilePath *string 18 | ) 19 | 20 | // importCmd represents the import command 21 | func importCommand() *cobra.Command { 22 | importCmd := &cobra.Command{ 23 | Use: "import", 24 | Short: "Import configurations to ChargePi.", 25 | Long: ``, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | importer := cfg.GetImporter() 28 | 29 | evseFlag := cmd.Flags().Lookup(settings.EvseFlag).Changed 30 | ocppFlag := cmd.Flags().Lookup(settings.OcppConfigPathFlag).Changed 31 | authFlag := cmd.Flags().Lookup(settings.AuthFileFlag).Changed 32 | settingsFlag := cmd.Flags().Lookup(settings.SettingsFlag).Changed 33 | 34 | if evseFlag { 35 | // If a directory is specified, (try to) import all the files in that directory. 36 | err := importer.ImportEVSESettingsFromPath(*evseFolderPath) 37 | if err != nil { 38 | return fmt.Errorf("could not import EVSE settings: %v", err) 39 | } 40 | } 41 | 42 | // If the flag was set, import OCPP configuration to the ChargePi 43 | if ocppFlag { 44 | err := importer.ImportOcppConfigurationFromPath(ocpp.ProtocolVersion(*ocppVersionFlag), *ocppConfigurationFilePath) 45 | if err != nil { 46 | return fmt.Errorf("could not import OCPP configuration: %v", err) 47 | } 48 | } 49 | 50 | // If the flag was set, import tags to the database. 51 | if authFlag { 52 | err := importer.ImportLocalAuthListFromPath(*authFilePath) 53 | if err != nil { 54 | return fmt.Errorf("could not import tags: %v", err) 55 | } 56 | } 57 | 58 | if settingsFlag { 59 | err := importer.ImportChargePointSettingsFromPath(*importSettingsFilePath) 60 | if err != nil { 61 | return fmt.Errorf("could not import settings: %v", err) 62 | } 63 | } 64 | 65 | return nil 66 | }, 67 | } 68 | 69 | evseFolderPath = importCmd.Flags().String(settings.EvseFlag, "", "evse folder path") 70 | ocppConfigurationFilePath = importCmd.Flags().String(settings.OcppConfigPathFlag, "", "OCPP config file path") 71 | ocppVersionFlag = importCmd.Flags().StringP(settings.OcppVersion, "v", "1.6", "OCPP config file path") 72 | authFilePath = importCmd.Flags().String(settings.AuthFileFlag, "", "authorization file path") 73 | importSettingsFilePath = importCmd.Flags().String(settings.SettingsFlag, "", "settings file path") 74 | 75 | return importCmd 76 | } 77 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 5 | "github.com/ChargePi/ChargePi-go/pkg/observability/logging" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "chargepi", 13 | Short: "ChargePi is an open-source Charge point project.", 14 | Long: ``, 15 | Run: func(cmd *cobra.Command, args []string) {}, 16 | } 17 | 18 | func init() { 19 | cobra.OnInitialize(func() { 20 | logging.Setup(log.StandardLogger(), settings.Logging{}, viper.GetBool(settings.Debug)) 21 | }) 22 | 23 | rootCmd.AddCommand(runCommand()) 24 | rootCmd.AddCommand(versionCommand()) 25 | rootCmd.AddCommand(exportCommand()) 26 | rootCmd.AddCommand(importCommand()) 27 | 28 | rootCmd.PersistentFlags().BoolP(settings.DebugFlag, "d", false, "debug mode") 29 | _ = viper.BindPFlag(settings.Debug, rootCmd.PersistentFlags().Lookup(settings.DebugFlag)) 30 | } 31 | 32 | func Execute() { 33 | err := rootCmd.Execute() 34 | if err != nil { 35 | log.WithError(err).Fatal("Unable to run") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/chargepoint" 5 | "github.com/ChargePi/ChargePi-go/internal/pkg/configuration" 6 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var ( 12 | // Basic configuration setting 13 | settingsFilePath string 14 | ) 15 | 16 | // runCommand is the command for the ChargePi core. 17 | func runCommand() *cobra.Command { 18 | runCmd := &cobra.Command{ 19 | Use: "run", 20 | Short: "Run the ChargePi core", 21 | Long: ``, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | configuration.InitSettings(settingsFilePath) 24 | 25 | debug := viper.GetBool(settings.Debug) 26 | mainSettings := configuration.GetSettings() 27 | 28 | // Run the charge point 29 | chargepoint.Run(debug, mainSettings) 30 | }, 31 | } 32 | 33 | runCmd.Flags().StringVar(&settingsFilePath, settings.SettingsFlag, "", "config file path") 34 | runCmd.Flags().String(settings.ApiAddressFlag, "localhost:4269", "listen address") 35 | 36 | _ = viper.BindPFlag(settings.ApiAddress, runCmd.Flags().Lookup(settings.ApiAddressFlag)) 37 | 38 | return runCmd 39 | } 40 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | chargePoint "github.com/ChargePi/ChargePi-go/internal/pkg/models/charge-point" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // versionCmd represents the version command 10 | func versionCommand() *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "version", 13 | Short: "Version of ChargePi", 14 | Long: ``, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | log.Infof("ChargePi version: %s", chargePoint.FirmwareVersion) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /deployments/docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | chargepi: 4 | build: 5 | dockerfile: ../../build/Dockerfile 6 | context: ../.. 7 | target: chargepi 8 | restart: always 9 | volumes: 10 | - data:/tmp/chargepi 11 | devices: 12 | - /dev/ttyAMA0:/dev/ttyAMA0 13 | - /dev/mem:/dev/mem 14 | privileged: true 15 | volumes: 16 | data: -------------------------------------------------------------------------------- /docs/configuration/flags.md: -------------------------------------------------------------------------------- 1 | # Flags and environment variables 2 | 3 | Here is a list of all available flags: 4 | 5 | | Flag | Short | Description | Default value | 6 | |:--------------:|:-----:|:--------------------------:|:-------------:| 7 | | `-settings` | / | Path to the settings file. | | 8 | | `-debug` | `--d` | Debug mode | false | 9 | | `-api` | `--a` | Expose the API | false | 10 | | `-api-address` | / | API address | "localhost" | 11 | | `-api-port` | / | API port | 4269 | 12 | 13 | Environment variables are created automatically thanks to [Viper](https://github.com/spf13/viper) and are prefixed 14 | with `CHARGEPI`. Only the settings file attributes are bound to the env variables as well as debug mode and API 15 | settings. Connectors do not have their attributes bound to environment variables. 16 | 17 | Example environment variable: `CHARGEPI_CHARGEPOINT_CONNECTIONSETTINGS_ID`. 18 | -------------------------------------------------------------------------------- /docs/configuration/importing-and-exporting.md: -------------------------------------------------------------------------------- 1 | # Importing and exporting configuration(s) 2 | 3 | There are three types of configurations: 4 | 5 | 1. [charge point](./configuration.md/#-charge-point-general-configuration) 6 | 2. [ocpp](./ocpp) 7 | 3. [evse](evse-configuration.md#the-evse-configuration) 8 | 9 | Each configuration can be imported and exported separately. Use the following commands to import and export 10 | configurations: 11 | 12 | ## Importing 13 | 14 | ```bash 15 | chargepi import 16 | ``` 17 | 18 | Available flags: 19 | 20 | | Flag | Description | Default value | 21 | |:--------:|:-------------------------------------------------------------------------:|:-------------:| 22 | | --evse | EVSE configuration folder path | | 23 | | --ocpp | OCPP configuration file path. Requires the --version flag to also be set. | | 24 | | --config | ChargePi configuration file path | | 25 | 26 | ## Exporting 27 | 28 | ```bash 29 | chargepi import 30 | ``` -------------------------------------------------------------------------------- /docs/configuration/ocpp/data-transfer-payloads.md: -------------------------------------------------------------------------------- 1 | # Custom data payloads 2 | 3 | ## OCPP 1.6 4 | 5 | ### Charge point model additional info 6 | 7 | This payload contains the type of the charge point and it's maximum power output for all EVSEs. 8 | It is sent to the Central System after it is approved by the Central System. 9 | 10 | ```json 11 | { 12 | "type": "AC", 13 | "maxPower": 6.0 14 | } 15 | ``` 16 | 17 | ### Connector details 18 | 19 | This payload contains maximum power output for an EVSE and the types of connectors the EVSE has. 20 | It is sent to the Central System after loading all the EVSE settings to the Charge point. 21 | 22 | ```json 23 | { 24 | "evseId": 1, 25 | "maxPower": 6.0, 26 | "connectors": [ 27 | { 28 | "id": 1, 29 | "type": "CCS-1" 30 | }, 31 | { 32 | "id": 2, 33 | "type": "ChaDeMo" 34 | } 35 | ] 36 | } 37 | ``` 38 | 39 | ### Displaying messages 40 | 41 | The ChargePi supports displaying messages on the display's screen. For forward-compatibility, all the payloads are the 42 | same as in OCPP 2.0.1 specification. 43 | 44 | The VendorID must be the same as the vendor id on the charge point. The message ID is the same as declared in the 45 | OCPP 2.0.1 specification. -------------------------------------------------------------------------------- /docs/contribution/client/ChargePi-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargePi/ChargePi-go/31aac5beb2761cf9757675ca2dd97606498d9b60/docs/contribution/client/ChargePi-overview.png -------------------------------------------------------------------------------- /docs/contribution/client/api.md: -------------------------------------------------------------------------------- 1 | # Client API 2 | 3 | The client comes with a GRPC API that enables other services to integrate with the client. One such use-case would be 4 | writing a frontend application that interacts with the client and displays the values for an ongoing transaction or 5 | starts/stops a transaction. 6 | 7 | To access the endpoint, it must be enabled through flags (`--a`) and will be exposed by default 8 | on `localhost:4269`. 9 | 10 | ## Endpoints 11 | 12 | Compiling the protobuf: 13 | 14 | ```bash 15 | buf generate 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/contribution/client/internal-structure.md: -------------------------------------------------------------------------------- 1 | # Architecture and design 2 | 3 | ![](./ChargePi-overview.png) 4 | 5 | ## Core 6 | 7 | ## Authentication manager 8 | 9 | ## EVSE Manager 10 | 11 | ### EVSE 12 | 13 | ### EVCC 14 | 15 | ### Power Meter 16 | 17 | ### Session 18 | 19 | ## Hardware 20 | 21 | ### Display 22 | 23 | ### Reader 24 | 25 | ### Indicator 26 | 27 | ## API 28 | 29 | ## Settings management -------------------------------------------------------------------------------- /docs/contribution/hardware/adding-support-for-hardware.md: -------------------------------------------------------------------------------- 1 | # ➡️Adding hardware support 2 | 3 | There are four hardware component groups that are included in the project: 4 | 5 | 1. Tag reader (NFC/RFID), 6 | 2. Display, 7 | 3. Status Indicator, 8 | 4. Power meter, 9 | 5. EVCC. 10 | 11 | These hardware components have corresponding interfaces that are included in the `ChargePointHandler` struct. This 12 | allows adding support for other models of hardware with similar functionalities. 13 | 14 | You're welcome to submit a Pull Request with any additional hardware model implementations! Be sure to test and document 15 | your changes, update the [supported hardware](../../hardware/hardware-support.md) table(s) with the new 16 | hardware model(s). It would be nice to have a wiring sketch or a connection table included for the new model(s). 17 | -------------------------------------------------------------------------------- /docs/contribution/hardware/display/display-contrib.md: -------------------------------------------------------------------------------- 1 | ## 🖥️ Display hardware 2 | 3 | All displays must implement the `Display` interface. It is recommended that you implement the interface in a new file named 4 | after the model of the display/LCD in the `hardware/display` package. 5 | 6 | Then you should add a **constant** named after the **model** of the display in the `display` file in the package and add a switch case with the implementation and the 7 | necessary logic that returns a pointer to the struct. 8 | 9 | ```golang 10 | package display 11 | 12 | import ( 13 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/notifications" 14 | ) 15 | 16 | const ( 17 | DriverHD44780 = "hd44780" 18 | ) 19 | 20 | // Display is an abstraction layer for concrete implementation of a display. 21 | type Display interface { 22 | DisplayMessage(message notifications.Message) 23 | Cleanup() 24 | Clear() 25 | GetType() string 26 | } 27 | 28 | ``` -------------------------------------------------------------------------------- /docs/contribution/hardware/display/i18n.md: -------------------------------------------------------------------------------- 1 | # 🌐 Display message translation 2 | 3 | All files, located in `internal/chargepoint/components/hardware/display/i18n/translations` and have a prefix 4 | of `active.` will be treated as translation files. The desired language should be specified in the `settings` 5 | file. If the selected language is not supported, English will be set as a default. 6 | 7 | All contributions are welcome! We're using [go-i18n](https://github.com/nicksnyder/go-i18n) for internationalization, so 8 | follow the instructions in their repository to create a new translation. The translated file 9 | should be added to `internal/chargepoint/components/hardware/display/i18n/translations` folder. 10 | 11 | ## 🌐 Supported languages 12 | 13 | | Language | Supported | 14 | |:---------:|:---------:| 15 | | English | ✔ | 16 | | Slovenian | ✔ | -------------------------------------------------------------------------------- /docs/contribution/hardware/evcc/evcc-contrib.md: -------------------------------------------------------------------------------- 1 | ## ⚡ EVCC 2 | 3 | All EVCCs must implement the `EVCC` interface. Every time a new EVCC is added, the Init method is called. All the 4 | communication must be established in that method. 5 | 6 | Add a constant for each implementation. The current state should be persisted in the struct and the GetType should 7 | return the exact type. 8 | 9 | ```golang 10 | package evcc 11 | 12 | import ( 13 | "context" 14 | 15 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 16 | ) 17 | 18 | const ( 19 | PhoenixEMCPPPETH = "EM-CP-PP-ETH" 20 | Relay = "Relay" 21 | Western = "Western" 22 | ) 23 | 24 | type EVCC interface { 25 | Init(ctx context.Context) error 26 | EnableCharging() error 27 | DisableCharging() 28 | SetMaxChargingCurrent(value float64) error 29 | GetMaxChargingCurrent() float64 30 | Lock() 31 | Unlock() 32 | GetState() CarState 33 | GetError() string 34 | Cleanup() error 35 | GetType() string 36 | GetStatusChangeChannel() <-chan StateNotification 37 | Reset() 38 | } 39 | 40 | ``` -------------------------------------------------------------------------------- /docs/contribution/hardware/indicator/indicator-contrib.md: -------------------------------------------------------------------------------- 1 | ## Indicator hardware 2 | 3 | All indicators should implement the `Indicator` interface. Initialize all the communication when creating a new struct. 4 | 5 | ```golang 6 | package indicator 7 | 8 | // color constants 9 | const ( 10 | Off = Color("Off") 11 | White = Color("White") 12 | Red = Color("Red") 13 | Green = Color("Green") 14 | Blue = Color("Blue") 15 | Yellow = Color("Yellow") 16 | Orange = Color("Orange") 17 | ) 18 | 19 | // Supported types 20 | const ( 21 | TypeWS281x = "WS281x" 22 | ) 23 | 24 | type ( 25 | Color string 26 | 27 | // Indicator is an abstraction layer for connector status indication, usually an RGB LED strip. 28 | Indicator interface { 29 | DisplayColor(index int, color Color) error 30 | Blink(index int, times int, color Color) error 31 | Cleanup() 32 | GetType() string 33 | } 34 | ) 35 | 36 | ``` -------------------------------------------------------------------------------- /docs/contribution/hardware/powerMeter/power-meter-contrib.md: -------------------------------------------------------------------------------- 1 | ## ⚡ Power meters 2 | 3 | All power meters must implement the `PowerMeter` interface. They should be able to, in one form or another, provide the 4 | basic functionality of a power meter: read the current, voltage and energy from the Power Meter hardware. 5 | 6 | Various communication protocols are used to get the data from the reader. All communication should be initialized in the 7 | `Init` method. The init method gets called before the program calls any of other methods. 8 | 9 | When adding a new power meter, declare a constant in the `power-meter` file. 10 | 11 | ```golang 12 | package powerMeter 13 | 14 | import ( 15 | "context" 16 | 17 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 18 | ) 19 | 20 | // Supported power meters 21 | const ( 22 | TypeC5460A = "cs5460a" 23 | ) 24 | 25 | // PowerMeter is an abstraction for measurement hardware. 26 | type PowerMeter interface { 27 | Init(ctx context.Context) error 28 | Reset() 29 | GetEnergy() types.SampledValue 30 | GetPower() types.SampledValue 31 | GetCurrent(phase int) types.SampledValue 32 | GetVoltage(phase int) types.SampledValue 33 | GetRMSCurrent(phase int) types.SampledValue 34 | GetRMSVoltage(phase int) types.SampledValue 35 | GetType() string 36 | } 37 | 38 | ``` -------------------------------------------------------------------------------- /docs/contribution/hardware/reader/reader-contrib.md: -------------------------------------------------------------------------------- 1 | ## 💳 Reader hardware 2 | 3 | All readers must implement the `Reader` interface. It is recommended that you implement the interface in a new file 4 | named after the model of the reader in the `hardware/reader` package. 5 | 6 | Then you should add a **constant** named after the **model** of the reader in the `reader` file in the package. 7 | 8 | ```golang 9 | package reader 10 | 11 | import ( 12 | "context" 13 | 14 | log "github.com/sirupsen/logrus" 15 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 16 | ) 17 | 18 | // Supported readers - by libnfc 19 | const ( 20 | PN532 = "PN532" 21 | ACR122 = "ACR122" 22 | PN533 = "PN533" 23 | BR500 = "BR500" 24 | R502 = "R502" 25 | ) 26 | 27 | // Reader is an abstraction for an RFID/NFC tag reader. 28 | type Reader interface { 29 | ListenForTags(ctx context.Context) 30 | Cleanup() 31 | Reset() 32 | GetTagChannel() <-chan string 33 | GetType() string 34 | } 35 | ``` -------------------------------------------------------------------------------- /docs/contribution/ui/ui.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargePi/ChargePi-go/31aac5beb2761cf9757675ca2dd97606498d9b60/docs/contribution/ui/ui.md -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Hardware 4 | 5 | ## Software 6 | 7 | ### Installation 8 | 9 | ### Configuration 10 | -------------------------------------------------------------------------------- /docs/getting-started/guides/assembly/WiringSketch_eng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargePi/ChargePi-go/31aac5beb2761cf9757675ca2dd97606498d9b60/docs/getting-started/guides/assembly/WiringSketch_eng.png -------------------------------------------------------------------------------- /docs/getting-started/guides/assembly/bill-of-materials.md: -------------------------------------------------------------------------------- 1 | # 📝 Bill of materials 2 | 3 | ## ❗ Note: 4 | 5 | The bill of materials can vary, depending on your preferences, part availability and price. The aim of the project is to 6 | make a charging point out of off-the-shelf hardware that is widely available. Also, the list is not in any way perfect. 7 | 8 | The list might not be suitable, if you're trying to make a charging point for EVs. In this case, you will need a proper 9 | EV Charging controller and safety components. 10 | 11 | ## Enclosure and wiring 12 | 13 | | Item | # | 14 | |:---------------------------------------------------:|:----------------------------------:| 15 | | Schneider Electric Kaedra with 4 openings (or more) | 1x | 16 | | Schneider Electric Schuko outlet for Kaedra | 4x (depends on the electrical box) | 17 | | Terminal/crimp connectors | a lot | 18 | | 1,5 mm2 or any 10A rated wire | around 20m | 19 | | Schuko plug | 1 | 20 | 21 | ## Electronics 22 | 23 | | Item | # | 24 | |:-----------------------:|:--------------------:| 25 | | ETIMat6 C10 | 1 | 26 | | ETIMat6 B6 | 1 | 27 | | Raspberry Pi 4 2GB | 1 | 28 | | 4-Relay module 230V 10A | 1 | 29 | | Huawei LTE modem | 1 | 30 | | CS5460A power meter | 4x (for each outlet) | 31 | | PN532 NFC/RFID reader | 1 | 32 | | WS281x LED strip | a few meters | 33 | 34 | ## Misc 35 | 36 | | Item | # | 37 | |:------------------------------------:|:---:| 38 | | M2,5 20mm screws | 4 | 39 | | M2 or M2,5 6mm screws | 2 | 40 | | __optionally__ 3D printed DIN mounts | 2 | 41 | 42 | ## Wiring diagram 43 | 44 | ![](WiringSketch_eng.png) -------------------------------------------------------------------------------- /docs/getting-started/installation/libraries.md: -------------------------------------------------------------------------------- 1 | # 💻 Installing libraries 2 | 3 | ## With the installation script 4 | 5 | The process of installing the dependencies was automated by creating a [script](../../../scripts/install-dependencies.sh) 6 | for installing the necessary dependencies. The script has the following arguments: 7 | 8 | | Argument | Default value | Description | 9 | |:--------:|:-------------:|:--------------------------------------------------------------------------------------------------------------:| 10 | | 0 | "pn532_i2c" | The libnfc configuration for PN532 RFID card reader. Valid values are: `pn532_uart`, `pn532_i2c`, `pn532_spi`. | 11 | | 1 | false (0) | Whether or not to install Go on the system using a shell script. | 12 | 13 | Example usage: 14 | 15 | ```bash 16 | cd ~/ChargePi-go/docs 17 | chmod +x install-dependencies.sh 18 | ./install-dependencies.sh pn532_i2c 1 19 | ``` 20 | 21 | ## Building libnfc for PN532 22 | 23 | 1. Get and extract the libnfc: 24 | 25 | ```bash 26 | cd ~ 27 | mkdir libnfc && cd libnfc/ 28 | wget https://github.com/nfc-tools/libnfc/releases/download/libnfc-1.8.0/libnfc-1.8.0.tar.bz2 29 | tar -xvjf libnfc-1.8.0.tar.bz2 30 | ``` 31 | 32 | **Next two steps may vary for your reader and communication protocol** 33 | 34 | 2. Create PN532 configuration: 35 | 36 | ```bash 37 | cd libnfc-1.8.0 38 | sudo mkdir /etc/nfc 39 | sudo mkdir /etc/nfc/devices.d 40 | sudo cp contrib/libnfc/pn532_uart_on_rpi.conf.sample /etc/nfc/devices.d/pn532_uart_on_rpi.conf 41 | sudo nano /etc/nfc/devices.d/pn532_uart_on_rpi.conf 42 | ``` 43 | 44 | 3. Add the line at the end of the file: 45 | 46 | ```text 47 | allow_intrusive_scan = true 48 | ``` 49 | 50 | 4. Install dependencies and configure: 51 | 52 | ```bash 53 | sudo apt-get install autoconf 54 | sudo apt-get install libtool 55 | sudo apt-get install libpcsclite-dev libusb-dev 56 | autoreconf -vis 57 | ./configure --with-drivers=pn532_uart --sysconfdir=/etc --prefix=/usr 58 | ``` 59 | 60 | 5. Build the library: 61 | 62 | ```bash 63 | sudo make clean 64 | sudo make install all 65 | ``` 66 | 67 | ## Installing rpi-ws281x library 68 | 69 | Follow the instructions from the [maintainer's repository](https://github.com/jgarff/rpi_ws281x). 70 | 71 | **TLDR; Use the instructions here** 72 | 73 | ```bash 74 | git clone https://github.com/jgarff/rpi_ws281x 75 | cd rpi_ws281x 76 | mkdir build 77 | cd build 78 | cmake -D BUILD_SHARED=OFF -D BUILD_TEST=ON .. 79 | cmake --build . 80 | sudo make install 81 | ``` 82 | 83 | To be able to use this C library in Go, it must be installed. You can do this by copying `*.h` to `/usr/local/include` 84 | and `'.a` files to `/usr/local/lib`. If not working, check the Go wrapper library 85 | instructions [here](https://github.com/rpi-ws281x/rpi-ws281x-go). 86 | -------------------------------------------------------------------------------- /docs/getting-started/installation/running-the-client.md: -------------------------------------------------------------------------------- 1 | # 🏃 Running the ChargePi 2 | 3 | This guide assumes you already have a working OCPP management/central system. If you do not have one already, 4 | check out [SteVe](https://github.com/RWTH-i5-IDSG/steve) and set it up according to their guide. 5 | 6 | ## Standalone 7 | 8 | Before you run/connect the client, make sure the backend is available and the charge point is 9 | registered. Also, **libnfc** should be installed (a convenience script is available). 10 | 11 | Running the client: 12 | 13 | ```bash 14 | go run -tags=rpi . 15 | ``` 16 | 17 | or compiling and executing the client: 18 | 19 | ```bash 20 | GOOS=linux 21 | GOARCH=arm64 22 | go build -o chargepi . 23 | ./chargepi 24 | ``` 25 | 26 | ## 🐳 Deploying on Docker 27 | 28 | 1. Build the client image on Docker: 29 | 30 | ```bash 31 | cd ChargePi/client 32 | docker build -t chargepi . 33 | ``` 34 | 35 | or pull it from Docker Hub: 36 | ```bash 37 | docker pull xblaz3kx/ChargePi-go:latest 38 | ``` 39 | 40 | 2. Run the container from built image: 41 | 42 | ```bash 43 | docker run --device /dev/ttyAMA0:/dev/ttyAMA0 --device /dev/mem:/dev/mem --privileged chargepi 44 | ``` 45 | 46 | ## Deploying using docker-compose 47 | 48 | 1. Build the ChargePi client: 49 | 50 | ```bash 51 | docker-compose -f ./deployments build . 52 | ``` 53 | 54 | 2. Run services in daemon mode: 55 | 56 | ```bash 57 | docker-compose -f ./deployments up -d 58 | ``` -------------------------------------------------------------------------------- /docs/getting-started/installation/service-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up ChargePi as a systemd service 2 | 3 | **Golang should be installed and the binary should be added to PATH variable.** 4 | 5 | 1. Create a systemd unit file 6 | ```bash 7 | sudo nano /etc/systemd/system/ChargePi.service 8 | ``` 9 | 10 | 2. Paste into ChargePi.service file: 11 | 12 | ```unit file 13 | [Unit] 14 | Description=ChargePi client 15 | After=network.target 16 | 17 | [Service] 18 | Type=simple 19 | WorkingDirectory= //ChargePi-go/ 20 | ExecStart=go build main.go && ./main 21 | Restart=on-failure 22 | KillSignal=SIGTERM 23 | 24 | [Install] 25 | WantedBy=multi-user.target 26 | ``` 27 | 3. Add the service to systemd: 28 | 29 | ```bash 30 | sudo chmod 640 /etc/systemd/system/ChargePi.service.service 31 | systemctl status ChargePi.service.service 32 | sudo systemctl daemon-reload 33 | sudo systemctl enable ChargePi.service.service 34 | sudo systemctl start ChargePi.service.service 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/hardware/display/hd44.md: -------------------------------------------------------------------------------- 1 | # HD44780 2 | 3 | ## Description 4 | 5 | The HD44780 LCD should be on I2C bus 1 with an address equal to 0x27. To find the I2C address, follow these steps: 6 | 7 | 1. Download i2c tools: 8 | 9 | ```bash 10 | sudo apt-get install -y i2c-tools 11 | ``` 12 | 13 | 2. Enable I2C interface and if needed, reboot. 14 | 15 | 3. Run the following command to get the I2C address: 16 | 17 | ```bash 18 | sudo i2cdetect -y 1 19 | ``` 20 | 21 | ## Wiring 22 | 23 | | RPI PIN | PCF8574 PIN | 24 | |:--------------------:|:-----------:| 25 | | 2 or any 5V pin | VCC | 26 | | 14 or any ground pin | GND | 27 | | 3 (GPIO 2) | SDA | 28 | | 5 (GPIO 3) | SCL | 29 | 30 | ## Example configuration 31 | 32 | TODO -------------------------------------------------------------------------------- /docs/hardware/evcc/relay.md: -------------------------------------------------------------------------------- 1 | ### Relay (or relay module) 2 | 3 | ## Description 4 | 5 | There are multiple simple relay options that could be used for the charging station, such as Solid state relays, 6 | contactors, etc. Choose your option according to your needs. 7 | 8 | Be very careful with the options and consult a professional, as it may become electrical and fire hazard. 9 | 10 | It is highly recommended splitting both GND and VCC between relays or using a relay module. 11 | 12 | ## Wiring 13 | 14 | | RPI PIN | RELAY PIN | 15 | |-----------------------------------|:---------:| 16 | | 4 or any 5V pin | VCC | 17 | | 20 or any ground pin | GND | 18 | | 37 (GPIO 26) or any free GPIO pin | S/Enable | 19 | 20 | ## Example configuration 21 | 22 | ```yaml 23 | evseId: 1 24 | maxPower: 1.7 25 | connectors: 26 | - connectorId: 1 27 | type: Schuko 28 | status: Available 29 | evcc: 30 | type: Relay 31 | relayPin: 26 32 | inverseLogic: false 33 | powerMeter: 34 | enabled: false 35 | type: CS5460A 36 | spi: 37 | bus: 0 38 | pin: 25 39 | cs5460a: 40 | shuntOffset: 0.055 41 | voltageDividerOffset: 1333 42 | ``` -------------------------------------------------------------------------------- /docs/hardware/hardware-support.md: -------------------------------------------------------------------------------- 1 | # Supported hardware 2 | 3 | ## RFID/NFC readers 4 | 5 | | Reader | Is supported | 6 | |:------:|:------------:| 7 | | PN532 | ✔ | 8 | 9 | ## Displays 10 | 11 | | Display | Is supported | 12 | |:-------:|:------------:| 13 | | HD44780 | ✔ | 14 | 15 | ## EVCC 16 | 17 | EV charging controller (EVCC) controls the communication with the EV and allows or denies the charging. It can also set 18 | the charging current limit. 19 | 20 | | EVCC | Is supported | 21 | |:---------------------:|:------------:| 22 | | Relay | ✔ | 23 | | Phoenix Contact EVSEs | Planned | 24 | 25 | ## Power meters 26 | 27 | | Power meter | Is supported | 28 | |:-----------:|:------------:| 29 | | CS5460A | ✔ | 30 | | ETI | Planned | 31 | 32 | ## Indicators 33 | 34 | | Indicator | Is supported | 35 | |:---------:|:------------:| 36 | | WS2812b | ✔ | 37 | | WS2811 | ✔ | 38 | 39 | ## Contributing 40 | 41 | If you want to add support for any type of hardware, read the 42 | contribution [guide](../contribution/hardware/adding-support-for-hardware.md). -------------------------------------------------------------------------------- /docs/hardware/indicator/ws281x.md: -------------------------------------------------------------------------------- 1 | # WS2811 and WS2812b 2 | 3 | ## Description 4 | 5 | The WS281x LED strip comes in multiple voltage variants. It is recommended to use the 5V variant, since the Raspberry Pi 6 | provides the 5V and sufficient amperage to power the strip instead of using an external power supply. 7 | 8 | ## Wiring 9 | 10 | | RPI PIN | WS281x PIN | 11 | |:------------:|:----------:| 12 | | Any 5V pin | VCC | 13 | | Any GND pin | GND | 14 | | 32 (GPIO 12) | Data | 15 | 16 | ## Example configuration 17 | 18 | TODO -------------------------------------------------------------------------------- /docs/hardware/misc/4g-modem/modem-setup.md: -------------------------------------------------------------------------------- 1 | # 🛠️ Configuring mobile connectivity 2 | 3 | ## Setting up & running Sakis3G 4 | 5 | 1. Update and upgrade: 6 | 7 | ```bash 8 | sudo apt-get update & sudo apt-get upgrade 9 | ``` 10 | 11 | 2. Install the Sakis3g client: 12 | 13 | ```bash 14 | sudo apt-get install ppp 15 | wget "http://raspberry-at-home.com/files/sakis3g.tar.gz" 16 | sudo mkdir /usr/bin/modem3g 17 | sudo chmod +x /usr/bin/modem3g 18 | sudo cp sakis3g.tar.gz /usr/bin/modem3g 19 | cd /usr/bin/modem3g 20 | ``` 21 | 22 | 3. Run the client interactively: 23 | 24 | ```bash 25 | sudo /usr/bin/modem3g/sakis3g connect --interactive 26 | ``` 27 | 28 | 4. Get default settings for your modem: 29 | 30 | ```bash 31 | lsusb 32 | ``` 33 | 34 | ## Running as a service 35 | 36 | 1. Paste default settings in a file: 37 | 38 | ```bash 39 | sudo nano /etc/sakis3g.conf 40 | ``` 41 | 42 | Config file example: 43 | 44 | ```text 45 | USBDRIVER="option" 46 | USBINTERFACE="0" 47 | APN="internet" 48 | MODEM="12d1:155e" 49 | ``` 50 | 51 | 2. Make a systemd service file: 52 | 53 | ```bash 54 | sudo nano /etc/systemd/system/modem-connection.service 55 | ``` 56 | 57 | 3. Paste into modem-connection.service file: 58 | 59 | ```unit file 60 | [Unit] 61 | Description=Modem connection service 62 | 63 | [Service] 64 | Type=simple 65 | ExecStart=/usr/bin/modem3g/sakis3g --sudo connect 66 | Restart=on-failure 67 | RestartSec=5 68 | KillMode=process 69 | 70 | [Install] 71 | WantedBy=multi-user.target 72 | ``` 73 | 74 | 4. Give permissions and add services to **systemd**: 75 | 76 | ```bash 77 | sudo chmod 640 /etc/systemd/system/modem-connection.service 78 | systemctl status modem-connection.service 79 | sudo systemctl daemon-reload 80 | sudo systemctl enable modem-connection.service 81 | sudo systemctl start modem-connection.service 82 | ``` 83 | 84 | ### References: 85 | 86 | * [Sakis3g client](http://raspberry-at-home.com/installing-3g-modem/#more-138) 87 | * [Systemd services](https://www.howtogeek.com/687970/how-to-run-a-linux-program-at-startup-with-systemd/) 88 | * [Detailed Modem tutorial](https://lawrencematthew.wordpress.com/2013/08/07/connect-raspberry-pi-to-a-3g-network-automatically-during-its-boot/) -------------------------------------------------------------------------------- /docs/hardware/power-meter/cs5460a.md: -------------------------------------------------------------------------------- 1 | # CS5460A (Single phase only) 2 | 3 | ## Description 4 | 5 | The CS5460A power meter is used for single phase installations only. It also needs a voltage divider and a shunt 6 | resistor to properly measure the current and voltage. 7 | 8 | ## Wiring 9 | 10 | | RPI PIN | CS5460A PIN | 11 | |:--------------------:|:-----------:| 12 | | 4 or 2 | VCC | 13 | | 25 or any ground pin | GND | 14 | | Any free pin | CE/CS | 15 | | 40 (GPIO 21) | SCK | 16 | | 38 (GPIO 20) | MOSI | 17 | | 35 (GPIO 19) | MISO | 18 | 19 | ## Example configuration 20 | 21 | TODO -------------------------------------------------------------------------------- /docs/hardware/readers/pn532.md: -------------------------------------------------------------------------------- 1 | # PN532 2 | 3 | ## Description 4 | 5 | The PN532 reader can communicate through UART/I2C/SPI. The client uses the NFC go library, which is a wrapper for libnfc 6 | 1.8.0 (and above). You could use any other libnfc compatible NFC/RFID reader, but the configuration steps as well as 7 | wiring could vary. 8 | 9 | ## Wiring 10 | 11 | The pinout varies depending on your preferred communication protocol. This pinout is used for UART. 12 | 13 | | RPI PIN | PN532 PIN | 14 | |:-------:|:---------:| 15 | | 5V | VCC | 16 | | GND | GND | 17 | | GPIO 14 | TX | 18 | | GPIO 15 | RX | 19 | 20 | ## Example configuration 21 | 22 | TODO -------------------------------------------------------------------------------- /internal/api/grpc/auth.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | tagsv1 "github.com/ChargePi/ChargePi-go/gen/proto/tags/v1" 7 | "github.com/ChargePi/ChargePi-go/internal/auth" 8 | "github.com/golang/protobuf/ptypes/empty" 9 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 10 | "google.golang.org/protobuf/types/known/timestamppb" 11 | ) 12 | 13 | type AuthService struct { 14 | tagsv1.UnimplementedTagServiceServer 15 | tagManager auth.Manager 16 | } 17 | 18 | func NewAuthService(tagManager auth.Manager) *AuthService { 19 | return &AuthService{ 20 | tagManager: tagManager, 21 | } 22 | } 23 | 24 | func (s *AuthService) GetAuthorizedCards(ctx context.Context, empty *empty.Empty) (*tagsv1.GetAuthorizedCardsResponse, error) { 25 | response := &tagsv1.GetAuthorizedCardsResponse{ 26 | AuthorizedCards: []*tagsv1.AuthorizedCard{}, 27 | } 28 | 29 | // Get all tags from the database 30 | tags := s.tagManager.GetTags() 31 | 32 | // Convert the tags to the gRPC response 33 | for _, tag := range tags { 34 | var timestamp *timestamppb.Timestamp 35 | if tag.IdTagInfo.ExpiryDate != nil { 36 | timestamp = timestamppb.New(tag.IdTagInfo.ExpiryDate.Time) 37 | } 38 | 39 | card := &tagsv1.AuthorizedCard{ 40 | TagId: tag.IdTag, 41 | Status: string(tag.IdTagInfo.Status), 42 | ExpiryDate: timestamp, 43 | } 44 | response.AuthorizedCards = append(response.AuthorizedCards, card) 45 | } 46 | 47 | return response, nil 48 | } 49 | 50 | func (s *AuthService) AddAuthorizedCards(ctx context.Context, request *tagsv1.AddAuthorizedCardsRequest) (*tagsv1.AddAuthorizedCardsResponse, error) { 51 | response := &tagsv1.AddAuthorizedCardsResponse{Status: []string{}} 52 | 53 | for _, tag := range request.GetAuthorizedCards() { 54 | // Add the tag to the database 55 | err := s.tagManager.AddTag(tag.GetTagId(), types.NewIdTagInfo(types.AuthorizationStatus(tag.GetStatus()))) 56 | if err != nil { 57 | response.Status = append(response.Status, "Failed") 58 | continue 59 | } 60 | 61 | response.Status = append(response.Status, "Success") 62 | } 63 | 64 | return response, nil 65 | } 66 | 67 | func (s *AuthService) RemoveAuthorizedCard(ctx context.Context, request *tagsv1.RemoveAuthorizedCardRequest) (*tagsv1.RemoveAuthorizedCardResponse, error) { 68 | response := &tagsv1.RemoveAuthorizedCardResponse{ 69 | Status: "Failed", 70 | } 71 | 72 | // Remove the tag from the database 73 | err := s.tagManager.RemoveTag(request.GetTagId()) 74 | if err != nil { 75 | return response, nil 76 | } 77 | 78 | response.Status = "Success" 79 | return response, nil 80 | } 81 | 82 | func (s *UserHandler) mustEmbedUnimplementedTagServer() { 83 | } 84 | -------------------------------------------------------------------------------- /internal/api/grpc/charge_point.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | grpc "github.com/ChargePi/ChargePi-go/gen/proto/charge_point/v1" 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/charge-point" 8 | cfg "github.com/ChargePi/ChargePi-go/internal/pkg/settings" 9 | "github.com/golang/protobuf/ptypes/empty" 10 | ) 11 | 12 | type ChargePointHandler struct { 13 | grpc.UnimplementedChargePointServiceServer 14 | point chargePoint.ChargePoint 15 | settingsManager cfg.Manager 16 | } 17 | 18 | func NewChargePointService(point chargePoint.ChargePoint, settingsManager cfg.Manager) *ChargePointHandler { 19 | return &ChargePointHandler{ 20 | point: point, 21 | settingsManager: settingsManager, 22 | } 23 | } 24 | 25 | func (s *ChargePointHandler) Restart(ctx context.Context, request *grpc.RestartRequest) (*empty.Empty, error) { 26 | err := s.point.Reset(request.Type) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &empty.Empty{}, nil 32 | } 33 | 34 | func (s *ChargePointHandler) ChangeChargePointDetails(ctx context.Context, request *grpc.ChangeChargePointDetailsRequest) (*grpc.ChangeChargePointDetailsResponse, error) { 35 | response := &grpc.ChangeChargePointDetailsResponse{} 36 | 37 | return response, nil 38 | } 39 | 40 | func (s *ChargePointHandler) GetVersion(ctx context.Context, e *empty.Empty) (*grpc.GetVersionResponse, error) { 41 | return &grpc.GetVersionResponse{ 42 | Version: s.point.GetVersion(), 43 | }, nil 44 | } 45 | 46 | func (s *ChargePointHandler) GetStatus(ctx context.Context, e *empty.Empty) (*grpc.GetStatusResponse, error) { 47 | return &grpc.GetStatusResponse{ 48 | Connected: s.point.IsConnected(), 49 | Status: s.point.GetStatus(), 50 | }, nil 51 | } 52 | 53 | func (s *ChargePointHandler) mustEmbedUnimplementedChargePointServer() { 54 | } 55 | -------------------------------------------------------------------------------- /internal/api/grpc/connectivity.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ChargePi/ChargePi-go/gen/proto/connection/v1" 7 | "google.golang.org/protobuf/types/known/emptypb" 8 | ) 9 | 10 | type ConnectivityHandler struct { 11 | connectionv1.UnimplementedConnectionServiceServer 12 | } 13 | 14 | func NewConnectivityHandler() *ConnectivityHandler { 15 | return &ConnectivityHandler{} 16 | } 17 | 18 | func (c *ConnectivityHandler) GetConnectionDetails(ctx context.Context, empty *emptypb.Empty) (*connectionv1.GetConnectionDetailsResponse, error) { 19 | //TODO implement me 20 | panic("implement me") 21 | } 22 | 23 | func (c *ConnectivityHandler) ChangeConnectionDetails(ctx context.Context, request *connectionv1.ChangeConnectionDetailsRequest) (*connectionv1.ChangeConnectionDetailsResponse, error) { 24 | //TODO implement me 25 | panic("implement me") 26 | } 27 | 28 | func (c *ConnectivityHandler) mustEmbedUnimplementedConnectionServiceServer() { 29 | } 30 | -------------------------------------------------------------------------------- /internal/api/grpc/logs.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | grpc "github.com/ChargePi/ChargePi-go/gen/proto/logs/v1" 5 | "github.com/golang/protobuf/ptypes/empty" 6 | ) 7 | 8 | type LogHandler struct { 9 | grpc.UnimplementedLogServiceServer 10 | } 11 | 12 | func NewLogHandler() *LogHandler { 13 | return &LogHandler{} 14 | } 15 | 16 | func (s *LogHandler) GetLogs(e *empty.Empty, server grpc.LogService_GetLogsServer) error { 17 | // todo either a file-watcher or pipe directly from logrus (hook)? 18 | return nil 19 | } 20 | 21 | func (s *LogHandler) mustEmbedUnimplementedLogServer() { 22 | } 23 | -------------------------------------------------------------------------------- /internal/api/grpc/server_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type grpcTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (s *grpcTestSuite) SetupTest() { 15 | } 16 | 17 | func TestGrpc(t *testing.T) { 18 | log.SetLevel(log.DebugLevel) 19 | suite.Run(t, new(grpcTestSuite)) 20 | } 21 | -------------------------------------------------------------------------------- /internal/api/grpc/user.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | grpc "github.com/ChargePi/ChargePi-go/gen/proto/users/v1" 7 | "github.com/ChargePi/ChargePi-go/internal/users/pkg/models" 8 | "github.com/ChargePi/ChargePi-go/internal/users/service" 9 | "github.com/golang/protobuf/ptypes/empty" 10 | ) 11 | 12 | type UserHandler struct { 13 | grpc.UnimplementedUserServiceServer 14 | userService service.Service 15 | } 16 | 17 | func NewUserHandler(userService service.Service) *UserHandler { 18 | return &UserHandler{ 19 | userService: userService, 20 | } 21 | } 22 | 23 | func (s *UserHandler) AddUser(ctx context.Context, user *grpc.User) (*grpc.AddUserResponse, error) { 24 | response := &grpc.AddUserResponse{ 25 | Status: "Failed", 26 | } 27 | 28 | err := s.userService.AddUser(user.GetUsername(), user.GetPassword(), user.GetRole()) 29 | if err == nil { 30 | response.Status = "Success" 31 | } 32 | 33 | return response, nil 34 | } 35 | 36 | func (s *UserHandler) GetUser(ctx context.Context, request *grpc.GetUserRequest) (*grpc.User, error) { 37 | user, err := s.userService.GetUser(request.GetUsername()) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return toUser(*user), nil 43 | } 44 | 45 | func (s *UserHandler) GetUsers(ctx context.Context, e *empty.Empty) (*grpc.GetUsersResponse, error) { 46 | response := &grpc.GetUsersResponse{} 47 | 48 | getUsers, err := s.userService.GetUsers() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | for _, user := range getUsers { 54 | response.Users = append(response.Users, toUser(user)) 55 | } 56 | 57 | return response, nil 58 | } 59 | 60 | func (s *UserHandler) RemoveUser(ctx context.Context, request *grpc.RemoveUserRequest) (*grpc.RemoveUserResponse, error) { 61 | response := &grpc.RemoveUserResponse{ 62 | Status: "Failed", 63 | } 64 | 65 | err := s.userService.DeleteUser(request.Username) 66 | if err == nil { 67 | response.Status = "Success" 68 | } 69 | 70 | return response, nil 71 | } 72 | 73 | func (s *UserHandler) mustEmbedUnimplementedUsersServer() { 74 | } 75 | 76 | func toUser(user models.User) *grpc.User { 77 | return &grpc.User{ 78 | Username: user.Username, 79 | Password: user.Password, 80 | Role: user.Role, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/api/http/application.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | healthcheck "github.com/tavsec/gin-healthcheck" 10 | "github.com/tavsec/gin-healthcheck/checks" 11 | "github.com/tavsec/gin-healthcheck/config" 12 | ginlogrus "github.com/toorop/gin-logrus" 13 | ) 14 | 15 | type App struct { 16 | router *gin.Engine 17 | } 18 | 19 | func NewAppServer() *App { 20 | ginRouter := gin.Default() 21 | 22 | return &App{ 23 | router: ginRouter, 24 | } 25 | } 26 | 27 | func (u *App) Serve(url string, checks ...checks.Check) { 28 | u.router.Use(ginlogrus.Logger(log.StandardLogger()), gin.Recovery()) 29 | 30 | // Configure healthcheck 31 | err := healthcheck.New(u.router, config.DefaultConfig(), checks) 32 | if err != nil { 33 | log.WithError(err).Panic("Failed to configure healthcheck") 34 | } 35 | 36 | err = u.router.Run(url) 37 | if err != nil && errors.Is(err, http.ErrServerClosed) { 38 | log.WithError(err).Fatal("Failed to start HTTP server") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/api/http/ui-server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/mandrigin/gin-spa/spa" 6 | log "github.com/sirupsen/logrus" 7 | ginlogrus "github.com/toorop/gin-logrus" 8 | ) 9 | 10 | type UI struct { 11 | router *gin.Engine 12 | } 13 | 14 | func NewUi() *UI { 15 | return &UI{ 16 | router: gin.Default(), 17 | } 18 | } 19 | 20 | func (u *UI) Serve(url string) { 21 | log.Infof("Starting UI at %s", url) 22 | u.router.Use(ginlogrus.Logger(log.StandardLogger()), spa.Middleware("/", "./ui/build")) 23 | 24 | err := u.router.Run(url) 25 | if err != nil { 26 | return 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/auth/cache/auth-cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/auth" 5 | "testing" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/database" 8 | "github.com/ChargePi/ChargePi-go/pkg/util" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type authCacheTestSuite struct { 14 | suite.Suite 15 | authCache *BadgerCache 16 | } 17 | 18 | func (s *authCacheTestSuite) SetupTest() { 19 | db := database.Get() 20 | s.authCache = NewAuthCache(db) 21 | s.authCache.RemoveCachedTags() 22 | } 23 | 24 | func (s *authCacheTestSuite) TestAddTag() { 25 | s.authCache.SetMaxCachedTags(1) 26 | 27 | tagId := util.GenerateRandomTag() 28 | s.authCache.AddTag(tagId, auth.okTag) 29 | 30 | // Test cached tag limit 31 | tagId = util.GenerateRandomTag() 32 | s.authCache.AddTag(tagId, auth.expiredTag) 33 | } 34 | 35 | func (s *authCacheTestSuite) TestRemoveCachedTags() { 36 | tagId1 := util.GenerateRandomTag() 37 | s.authCache.AddTag(tagId1, auth.okTag) 38 | 39 | tagId2 := util.GenerateRandomTag() 40 | s.authCache.AddTag(tagId2, auth.expiredTag) 41 | 42 | tagId3 := util.GenerateRandomTag() 43 | s.authCache.AddTag(tagId3, auth.blockedTag) 44 | 45 | s.authCache.RemoveCachedTags() 46 | 47 | _, err := s.authCache.GetTag(tagId1) 48 | s.Assert().Error(err) 49 | 50 | _, err = s.authCache.GetTag(tagId2) 51 | s.Assert().Error(err) 52 | 53 | _, err = s.authCache.GetTag(tagId3) 54 | s.Assert().Error(err) 55 | } 56 | 57 | func (s *authCacheTestSuite) TestGetTag() { 58 | tagId := util.GenerateRandomTag() 59 | s.authCache.AddTag(tagId, auth.okTag) 60 | tag, err := s.authCache.GetTag(tagId) 61 | s.Assert().NoError(err) 62 | s.Assert().EqualValues(*auth.okTag, *tag) 63 | 64 | tagId = util.GenerateRandomTag() 65 | _, err = s.authCache.GetTag(tagId) 66 | s.Assert().Error(err) 67 | } 68 | 69 | func TestAuthCache(t *testing.T) { 70 | log.SetLevel(log.DebugLevel) 71 | suite.Run(t, new(authCacheTestSuite)) 72 | } 73 | -------------------------------------------------------------------------------- /internal/auth/contst_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 7 | ) 8 | 9 | const () 10 | 11 | var ( 12 | okTag = &types.IdTagInfo{ 13 | ExpiryDate: types.NewDateTime(time.Now().Add(10 * time.Minute)), 14 | Status: types.AuthorizationStatusAccepted, 15 | } 16 | 17 | blockedTag = &types.IdTagInfo{ 18 | ExpiryDate: types.NewDateTime(time.Now().Add(40 * time.Minute)), 19 | Status: types.AuthorizationStatusBlocked, 20 | } 21 | 22 | expiredTag = &types.IdTagInfo{ 23 | ExpiryDate: types.NewDateTime(time.Date(1999, 1, 1, 1, 1, 1, 0, time.Local)), 24 | Status: types.AuthorizationStatusAccepted, 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /internal/auth/list/local-auth-list_test.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/auth" 5 | "testing" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/database" 8 | "github.com/ChargePi/ChargePi-go/pkg/util" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type localAuthListTestSuite struct { 14 | suite.Suite 15 | authList LocalAuthList 16 | } 17 | 18 | func (s *localAuthListTestSuite) SetupTest() { 19 | db := database.Get() 20 | s.authList = NewLocalAuthList(db, 10) 21 | } 22 | 23 | func (s *localAuthListTestSuite) TestAddTag() { 24 | tagId := util.GenerateRandomTag() 25 | err := s.authList.AddTag(tagId, auth.okTag) 26 | s.Assert().NoError(err) 27 | 28 | tagId = util.GenerateRandomTag() 29 | err = s.authList.AddTag(tagId, auth.blockedTag) 30 | s.Assert().NoError(err) 31 | 32 | tagId = util.GenerateRandomTag() 33 | err = s.authList.AddTag(tagId, nil) 34 | s.Assert().Error(err) 35 | 36 | } 37 | 38 | func (s *localAuthListTestSuite) TestUpdateTag() { 39 | err := s.authList.UpdateTag("", nil) 40 | s.Assert().NoError(err) 41 | 42 | err = s.authList.UpdateTag("", nil) 43 | s.Assert().NoError(err) 44 | 45 | err = s.authList.UpdateTag("", nil) 46 | s.Assert().NoError(err) 47 | } 48 | 49 | func (s *localAuthListTestSuite) TestRemoveTag() { 50 | tagId := util.GenerateRandomTag() 51 | err := s.authList.AddTag(tagId, auth.blockedTag) 52 | s.Require().NoError(err) 53 | 54 | err = s.authList.RemoveTag(tagId) 55 | s.Assert().NoError(err) 56 | 57 | err = s.authList.RemoveTag("") 58 | s.Assert().Error(err) 59 | 60 | tagId = util.GenerateRandomTag() 61 | err = s.authList.RemoveTag(tagId) 62 | s.Assert().Error(err) 63 | } 64 | 65 | func (s *localAuthListTestSuite) TestRemoveAll() { 66 | s.authList.RemoveAll() 67 | } 68 | 69 | func (s *localAuthListTestSuite) TestGetTag() { 70 | _, err := s.authList.GetTag("") 71 | s.Assert().NoError(err) 72 | } 73 | 74 | func (s *localAuthListTestSuite) TestGetTags() { 75 | tagList := s.authList.GetTags() 76 | s.Assert().NotEmpty(tagList) 77 | } 78 | 79 | func (s *localAuthListTestSuite) TestSetMaxTags() { 80 | } 81 | 82 | func (s *localAuthListTestSuite) TestVersion() { 83 | version := s.authList.GetVersion() 84 | s.Assert().EqualValues(1, version) 85 | 86 | s.authList.SetVersion(1) 87 | s.Assert().EqualValues(1, version) 88 | 89 | s.authList.SetVersion(0) 90 | version = s.authList.GetVersion() 91 | s.Assert().EqualValues(1, version) 92 | } 93 | 94 | func TestLocalAuth(t *testing.T) { 95 | log.SetLevel(log.DebugLevel) 96 | suite.Run(t, new(localAuthListTestSuite)) 97 | } 98 | -------------------------------------------------------------------------------- /internal/chargepoint/api.go: -------------------------------------------------------------------------------- 1 | package chargepoint 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/api/grpc" 5 | "github.com/ChargePi/ChargePi-go/internal/api/http" 6 | "github.com/ChargePi/ChargePi-go/internal/auth" 7 | "github.com/ChargePi/ChargePi-go/internal/evse/manager" 8 | chargePoint "github.com/ChargePi/ChargePi-go/internal/pkg/models/charge-point" 9 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 10 | cfg "github.com/ChargePi/ChargePi-go/internal/pkg/settings" 11 | userDatabase "github.com/ChargePi/ChargePi-go/internal/users/pkg/database" 12 | "github.com/ChargePi/ChargePi-go/internal/users/service" 13 | "github.com/dgraph-io/badger/v3" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // SetupApi Runs a gRPC API server at a specified address if it is enabled. The API is protected by an authentication layer. 18 | // Check the user manual for defaults. 19 | func SetupApi( 20 | db *badger.DB, 21 | api settings.Api, 22 | handler chargePoint.ChargePoint, 23 | tagManager auth.Manager, 24 | manager manager.Manager, 25 | settingsManager cfg.Manager, 26 | ) { 27 | if !api.Enabled { 28 | log.Info("API is disabled") 29 | return 30 | } 31 | 32 | // User database layer 33 | userDb := userDatabase.NewUserDb(db) 34 | 35 | // User service layer 36 | userService := service.NewUserService(userDb) 37 | 38 | // Expose the API endpoints 39 | server := grpc.NewServer(api, handler, tagManager, manager, settingsManager, userService) 40 | server.Run() 41 | } 42 | 43 | // SetupUi Runs a management UI server if enabled 44 | func SetupUi(uiSettings settings.Ui) { 45 | if !uiSettings.Enabled { 46 | log.Info("Management UI is disabled") 47 | return 48 | } 49 | 50 | ui := http.NewUi() 51 | ui.Serve(uiSettings.Address) 52 | } 53 | 54 | // Creates a healthcheck endpoint 55 | func setupHealthcheck() { 56 | log.Infof("Starting application healthcheck at localhost:8081") 57 | httpServer := http.NewAppServer() 58 | httpServer.Serve(":8081") 59 | } 60 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/charge-point-details.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | data "github.com/ChargePi/ChargePi-go/pkg/models/ocpp" 5 | "github.com/lorenzodonini/ocpp-go/ocpp" 6 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 7 | ) 8 | 9 | // sendHeartBeat Send a setHeartbeat to the central system. 10 | func (cp *ChargePoint) sendChargePointInfo() error { 11 | cp.logger.Info("Sending charge point information to the central system..") 12 | dataTransfer := core.NewDataTransferRequest(cp.info.OCPPDetails.Vendor) 13 | dataTransfer.Data = data.NewChargePointInfo(cp.info.Type, cp.info.MaxPower) 14 | // dataTransfer.MessageId 15 | 16 | return cp.sendRequest(dataTransfer, func(confirmation ocpp.Response, protoError error) { 17 | if protoError != nil { 18 | cp.logger.WithError(protoError).Warn("Error sending data") 19 | return 20 | } 21 | 22 | resp := confirmation.(*core.DataTransferConfirmation) 23 | if resp.Status == core.DataTransferStatusAccepted { 24 | cp.logger.Info("Sent additional charge point information") 25 | } 26 | }) 27 | } 28 | 29 | // sendEvses Send the EVSEs' configuration to the central system. 30 | func (cp *ChargePoint) sendEvses() { 31 | for _, evse := range cp.evseManager.GetEVSEs() { 32 | var connectors []data.Connector 33 | for _, connector := range evse.GetConnectors() { 34 | connectors = append(connectors, data.NewConnector(connector.ConnectorId, connector.Type)) 35 | } 36 | 37 | cp.SendEVSEsDetails(evse.GetEvseId(), float32(evse.GetMaxChargingPower()), connectors...) 38 | } 39 | } 40 | 41 | func (cp *ChargePoint) SendEVSEsDetails(evseId int, maxPower float32, connectors ...data.Connector) { 42 | logInfo := cp.logger.WithField("evseId", evseId) 43 | logInfo.Info("Sending EVSE details to the central system") 44 | 45 | dataTransfer := core.NewDataTransferRequest(cp.info.OCPPDetails.Vendor) 46 | dataTransfer.Data = data.NewEvseInfo(evseId, maxPower, connectors...) 47 | 48 | err := cp.sendRequest(dataTransfer, func(confirmation ocpp.Response, protoError error) { 49 | if protoError != nil { 50 | logInfo.WithError(protoError).Warn("Error sending data") 51 | return 52 | } 53 | 54 | resp := confirmation.(*core.DataTransferConfirmation) 55 | if resp.Status == core.DataTransferStatusAccepted { 56 | logInfo.Info("Sent additional charge point information") 57 | } 58 | }) 59 | if err != nil { 60 | logInfo.WithError(err).Warn("Error sending data") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/charge-point_test.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "github.com/stretchr/testify/suite" 5 | "testing" 6 | ) 7 | 8 | type chargePointTestSuite struct { 9 | suite.Suite 10 | cp *ChargePoint 11 | } 12 | 13 | func (s *chargePointTestSuite) SetupTest() { 14 | } 15 | 16 | func (s *chargePointTestSuite) TestRestoreState() { 17 | } 18 | 19 | func (s *chargePointTestSuite) TestNotifyConnectorStatus() { 20 | } 21 | 22 | func TestChargePoint(t *testing.T) { 23 | suite.Run(t, new(chargePointTestSuite)) 24 | } 25 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/firmware-updates.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" 4 | 5 | func (cp *ChargePoint) OnGetDiagnostics(request *firmware.GetDiagnosticsRequest) (confirmation *firmware.GetDiagnosticsConfirmation, err error) { 6 | 7 | // cp.diagnosticsManager.UploadLogs(request.Location, request.Retries, request.RetryInterval) 8 | return firmware.NewGetDiagnosticsConfirmation(), nil 9 | } 10 | 11 | func (cp *ChargePoint) OnUpdateFirmware(request *firmware.UpdateFirmwareRequest) (confirmation *firmware.UpdateFirmwareConfirmation, err error) { 12 | return firmware.NewUpdateFirmwareConfirmation(), nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/hardware_test.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ChargePi/ChargePi-go/pkg/indicator" 8 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | const ( 14 | exampleMessage = "exampleMessage" 15 | exampleMessage1 = "exampleMessage2" 16 | ) 17 | 18 | type hardwareTestSuite struct { 19 | suite.Suite 20 | cp *ChargePoint 21 | } 22 | 23 | func (s *hardwareTestSuite) SetupTest() { 24 | s.cp = new(ChargePoint) 25 | s.cp.logger = log.StandardLogger() 26 | } 27 | 28 | func (s *hardwareTestSuite) TestSendToLCD() { 29 | } 30 | 31 | func (s *hardwareTestSuite) TestDisplayLedStatus() { 32 | // Ok statuses 33 | s.cp.indicateStatusChange(1, core.ChargePointStatusCharging) 34 | s.cp.indicateStatusChange(1, core.ChargePointStatusFinishing) 35 | s.cp.indicateStatusChange(1, core.ChargePointStatusAvailable) 36 | s.cp.indicateStatusChange(1, core.ChargePointStatusFaulted) 37 | s.cp.indicateStatusChange(1, core.ChargePointStatusUnavailable) 38 | s.cp.indicateStatusChange(1, core.ChargePointStatusReserved) 39 | // Invalid status 40 | s.cp.indicateStatusChange(1, "") 41 | 42 | time.Sleep(time.Second) 43 | } 44 | 45 | func (s *hardwareTestSuite) TestIndicateCard() { 46 | // Ok indication 47 | s.cp.indicateCard(1, indicator.White) 48 | 49 | time.Sleep(time.Second) 50 | } 51 | 52 | func TestHardware(t *testing.T) { 53 | log.SetLevel(log.TraceLevel) 54 | suite.Run(t, new(hardwareTestSuite)) 55 | } 56 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/localauth.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/auth" 7 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" 8 | ) 9 | 10 | func (cp *ChargePoint) OnGetLocalListVersion(request *localauth.GetLocalListVersionRequest) (confirmation *localauth.GetLocalListVersionConfirmation, err error) { 11 | cp.logger.Infof("Received request %s", request.GetFeatureName()) 12 | version := cp.tagManager.GetAuthListVersion() 13 | res := localauth.NewGetLocalListVersionConfirmation(version) 14 | return res, nil 15 | } 16 | 17 | func (cp *ChargePoint) OnSendLocalList(request *localauth.SendLocalListRequest) (confirmation *localauth.SendLocalListConfirmation, err error) { 18 | cp.logger.Infof("Received request %s", request.GetFeatureName()) 19 | 20 | res := localauth.UpdateStatusFailed 21 | 22 | updateErr := cp.tagManager.UpdateLocalAuthList(request.ListVersion, request.UpdateType, request.LocalAuthorizationList) 23 | switch { 24 | case updateErr == nil: 25 | res = localauth.UpdateStatusAccepted 26 | case errors.Is(updateErr, auth.ErrLocalAuthListNotEnabled): 27 | res = localauth.UpdateStatusNotSupported 28 | } 29 | 30 | return localauth.NewSendLocalListConfirmation(res), nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/reservations.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/ChargePi/ChargePi-go/internal/evse/manager" 7 | 8 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/reservation" 9 | ) 10 | 11 | func (cp *ChargePoint) OnReserveNow(request *reservation.ReserveNowRequest) (confirmation *reservation.ReserveNowConfirmation, err error) { 12 | cp.logger.Infof("Received %s for %v", request.GetFeatureName(), request.ConnectorId) 13 | 14 | err = cp.evseManager.Reserve(request.ConnectorId, nil, request.ReservationId, request.IdTag) 15 | switch { 16 | case err == nil: 17 | timeFormat := fmt.Sprintf("%d:%d", request.ExpiryDate.Hour(), request.ExpiryDate.Minute()) 18 | _, schedulerErr := cp.scheduler.Every(1).Day().At(timeFormat).LimitRunsTo(1).Do(cp.evseManager.RemoveReservation, request.ReservationId) 19 | if schedulerErr != nil { 20 | return reservation.NewReserveNowConfirmation(reservation.ReservationStatusRejected), nil 21 | } 22 | 23 | return reservation.NewReserveNowConfirmation(reservation.ReservationStatusAccepted), nil 24 | case errors.Is(err, manager.ErrConnectorStatusInvalid): 25 | return reservation.NewReserveNowConfirmation(reservation.ReservationStatusOccupied), nil 26 | default: 27 | return reservation.NewReserveNowConfirmation(reservation.ReservationStatusRejected), nil 28 | } 29 | } 30 | 31 | func (cp *ChargePoint) OnCancelReservation(request *reservation.CancelReservationRequest) (confirmation *reservation.CancelReservationConfirmation, err error) { 32 | cp.logger.Infof("Received %s for %v", request.GetFeatureName(), request.ReservationId) 33 | status := reservation.CancelReservationStatusAccepted 34 | 35 | err = cp.evseManager.RemoveReservation(request.ReservationId) 36 | switch err { 37 | case nil: 38 | default: 39 | status = reservation.CancelReservationStatusRejected 40 | } 41 | 42 | return reservation.NewCancelReservationConfirmation(status), nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/reservations_test.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/scheduler" 8 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/reservation" 9 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | const ( 15 | reservationId = 1 16 | connectorId = 1 17 | tagId = "exampleTagId" 18 | ) 19 | 20 | type reservationTestSuite struct { 21 | suite.Suite 22 | cp *ChargePoint 23 | } 24 | 25 | func (s *reservationTestSuite) SetupTest() { 26 | s.cp = &ChargePoint{ 27 | logger: log.StandardLogger(), 28 | scheduler: scheduler.NewScheduler(), 29 | } 30 | } 31 | 32 | func (s *reservationTestSuite) TestReservation() { 33 | var ( 34 | expiryDate = types.NewDateTime(time.Now().Add(time.Minute)) 35 | ) 36 | 37 | response, err := s.cp.OnReserveNow(reservation.NewReserveNowRequest(connectorId, expiryDate, tagId, reservationId)) 38 | s.Assert().NoError(err) 39 | s.Assert().NotNil(response) 40 | s.Assert().EqualValues(reservation.ReservationStatusAccepted, response.Status) 41 | 42 | // No connector with connectorId 43 | response, err = s.cp.OnReserveNow(reservation.NewReserveNowRequest(2, expiryDate, tagId, reservationId)) 44 | s.Assert().NoError(err) 45 | s.Assert().NotNil(response) 46 | s.Assert().EqualValues(reservation.ReservationStatusUnavailable, response.Status) 47 | 48 | // Unable to reserve for whatever reason 49 | response, err = s.cp.OnReserveNow(reservation.NewReserveNowRequest(connectorId, expiryDate, tagId, 2)) 50 | s.Assert().NoError(err) 51 | s.Assert().NotNil(response) 52 | s.Assert().EqualValues(reservation.ReservationStatusRejected, response.Status) 53 | } 54 | 55 | func (s *reservationTestSuite) TestCancelReservation() { 56 | response, err := s.cp.OnCancelReservation(reservation.NewCancelReservationRequest(1)) 57 | s.Assert().NoError(err) 58 | s.Assert().NotNil(response) 59 | s.Assert().EqualValues(reservation.CancelReservationStatusAccepted, response.Status) 60 | 61 | // Something went wrong with the reservation 62 | response, err = s.cp.OnCancelReservation(reservation.NewCancelReservationRequest(reservationId)) 63 | s.Assert().NoError(err) 64 | s.Assert().NotNil(response) 65 | s.Assert().EqualValues(reservation.CancelReservationStatusRejected, response.Status) 66 | 67 | // No connector with the reservation 68 | response, err = s.cp.OnCancelReservation(reservation.NewCancelReservationRequest(2)) 69 | s.Assert().NoError(err) 70 | s.Assert().NotNil(response) 71 | s.Assert().EqualValues(reservation.CancelReservationStatusRejected, response.Status) 72 | } 73 | 74 | func TestReservation(t *testing.T) { 75 | log.SetLevel(log.DebugLevel) 76 | suite.Run(t, new(reservationTestSuite)) 77 | } 78 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/stop-charging.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/evse" 8 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/charge-point" 9 | "github.com/ChargePi/ChargePi-go/internal/pkg/util" 10 | "github.com/lorenzodonini/ocpp-go/ocpp" 11 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 12 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func (cp *ChargePoint) StopCharging(evseId, connectorId int, reason core.Reason) error { 17 | cpEvse, err := cp.evseManager.GetEVSE(evseId) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return cp.stopChargingConnector(cpEvse, reason) 23 | } 24 | 25 | // stopChargingConnector Stop charging a connector with the specified ID. Update the status(es), turn off the ConnectorImpl and calculate the energy consumed. 26 | func (cp *ChargePoint) stopChargingConnector(connector evse.EVSE, reason core.Reason) error { 27 | if util.IsNilInterfaceOrPointer(connector) { 28 | return chargePoint.ErrConnectorNil 29 | } 30 | 31 | logInfo := cp.logger.WithFields(log.Fields{ 32 | "evseId": connector.GetEvseId(), 33 | "reason": reason, 34 | }) 35 | 36 | // Check if the connector is already stopped 37 | session, err := cp.sessionManager.GetSession(connector.GetEvseId(), nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | transactionId, convErr := strconv.Atoi(session.TransactionId) 43 | if convErr != nil { 44 | return convErr 45 | } 46 | 47 | request := core.NewStopTransactionRequest( 48 | int(session.CalculateEnergyConsumptionWithAvgPower()), 49 | types.NewDateTime(time.Now()), 50 | transactionId, 51 | ) 52 | request.Reason = reason 53 | 54 | var callback = func(confirmation ocpp.Response, protoError error) { 55 | if protoError != nil { 56 | logInfo.WithError(protoError).Errorf("Server responded with error for stopping a transaction") 57 | return 58 | } 59 | 60 | logInfo.Info("Stopping transaction") 61 | 62 | // Stop charging on EVSE 63 | err = connector.StopCharging(reason) 64 | if err != nil { 65 | logInfo.WithError(err).Errorf("Unable to stop charging") 66 | return 67 | } 68 | 69 | err = cp.sessionManager.StopSession(session.TransactionId) 70 | if err != nil { 71 | logInfo.WithError(err).Warnf("Unable to stop session") 72 | } 73 | 74 | logInfo.Infof("Stopped charging at %s", time.Now()) 75 | } 76 | 77 | return cp.sendRequest(request, callback) 78 | } 79 | -------------------------------------------------------------------------------- /internal/chargepoint/v16/trigger-message_test.go: -------------------------------------------------------------------------------- 1 | package v16 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/notifications" 8 | "github.com/ChargePi/ChargePi-go/internal/pkg/scheduler" 9 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 10 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type triggerMessageTestSuite struct { 16 | suite.Suite 17 | cp *ChargePoint 18 | } 19 | 20 | func (s *triggerMessageTestSuite) SetupTest() { 21 | s.cp = &ChargePoint{ 22 | logger: log.StandardLogger(), 23 | scheduler: scheduler.NewScheduler(), 24 | } 25 | s.cp.scheduler.Clear() 26 | } 27 | 28 | func (s *triggerMessageTestSuite) TestTriggerMessage() { 29 | var ( 30 | connectorChan = make(chan notifications.StatusNotification, 2) 31 | ) 32 | 33 | // Set manager expectations 34 | 35 | numMessages := 0 36 | go func() { 37 | for { 38 | select { 39 | case <-connectorChan: 40 | numMessages++ 41 | } 42 | } 43 | }() 44 | 45 | response, err := s.cp.OnTriggerMessage(remotetrigger.NewTriggerMessageRequest("something")) 46 | s.Assert().NoError(err) 47 | s.Assert().NotNil(response) 48 | s.Assert().EqualValues(remotetrigger.TriggerMessageStatusNotImplemented, response.Status) 49 | 50 | response, err = s.cp.OnTriggerMessage(remotetrigger.NewTriggerMessageRequest(core.MeterValuesFeatureName)) 51 | s.Assert().NoError(err) 52 | s.Assert().NotNil(response) 53 | s.Assert().EqualValues(remotetrigger.TriggerMessageStatusNotImplemented, response.Status) 54 | 55 | response, err = s.cp.OnTriggerMessage(remotetrigger.NewTriggerMessageRequest(core.HeartbeatFeatureName)) 56 | s.Assert().NoError(err) 57 | s.Assert().NotNil(response) 58 | s.Assert().EqualValues(remotetrigger.TriggerMessageStatusAccepted, response.Status) 59 | s.Assert().Len(s.cp.scheduler.Jobs(), 1) 60 | 61 | s.cp.scheduler.Clear() 62 | 63 | time.Sleep(time.Second) 64 | 65 | response, err = s.cp.OnTriggerMessage(remotetrigger.NewTriggerMessageRequest(core.BootNotificationFeatureName)) 66 | s.Assert().NoError(err) 67 | s.Assert().NotNil(response) 68 | s.Assert().EqualValues(remotetrigger.TriggerMessageStatusAccepted, response.Status) 69 | s.Assert().Len(s.cp.scheduler.Jobs(), 1) 70 | 71 | s.cp.scheduler.Clear() 72 | 73 | // Get status of all connectors 74 | response, err = s.cp.OnTriggerMessage(remotetrigger.NewTriggerMessageRequest(core.StatusNotificationFeatureName)) 75 | s.Assert().NoError(err) 76 | s.Assert().NotNil(response) 77 | s.Assert().EqualValues(remotetrigger.TriggerMessageStatusAccepted, response.Status) 78 | 79 | time.Sleep(time.Second * 3) 80 | s.Assert().EqualValues(1, numMessages) 81 | 82 | // Get status of a single connector 83 | } 84 | 85 | func TestTriggerMessage(t *testing.T) { 86 | suite.Run(t, new(triggerMessageTestSuite)) 87 | } 88 | -------------------------------------------------------------------------------- /internal/evse/evcc.go: -------------------------------------------------------------------------------- 1 | package evse 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 5 | "github.com/ChargePi/ChargePi-go/pkg/evcc" 6 | ) 7 | 8 | func (evse *Impl) Lock() { 9 | evse.logger.Debugf("Locking EVCC") 10 | evse.evcc.Lock() 11 | } 12 | 13 | func (evse *Impl) Unlock() { 14 | evse.logger.Debugf("Unlocking EVCC") 15 | evse.evcc.Unlock() 16 | } 17 | 18 | func (evse *Impl) GetConnectors() []settings.Connector { 19 | evse.logger.Debugf("Getting connectors for EVSE") 20 | return evse.connectors 21 | } 22 | 23 | func (evse *Impl) AddConnector(connector settings.Connector) error { 24 | evse.logger.WithField("connectorId", connector.ConnectorId).Debug("Adding connector to EVSE") 25 | for _, c := range evse.connectors { 26 | // Do not add if they're the same connector 27 | if c.ConnectorId == connector.ConnectorId { 28 | return ErrConnectorExists 29 | } 30 | } 31 | 32 | evse.connectors = append(evse.connectors, connector) 33 | return nil 34 | } 35 | 36 | func (evse *Impl) GetEvcc() evcc.EVCC { 37 | evse.logger.Debugf("Getting EVCC") 38 | return evse.evcc 39 | } 40 | 41 | func (evse *Impl) SetEvcc(e evcc.EVCC) { 42 | evse.logger.Debugf("Setting EVCC") 43 | 44 | // Cleanup the previous EVCC 45 | err := evse.evcc.Cleanup() 46 | if err != nil { 47 | evse.logger.Errorf("Error cleaning up EVCC: %s", err) 48 | } 49 | 50 | evse.evcc = e 51 | } 52 | -------------------------------------------------------------------------------- /internal/evse/manager/manager-reservations.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/evse" 5 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func (m *managerImpl) GetEVSEWithReservationId(reservationId int) (evse.EVSE, error) { 10 | logInfo := m.logger.WithField("reservationId", reservationId) 11 | logInfo.Debugf("Finding evse with reservation id") 12 | 13 | evseId, isFound := m.reservations[reservationId] 14 | if !isFound || evseId == nil { 15 | return nil, ErrReservationNotFound 16 | } 17 | 18 | evse, err := m.GetEVSE(*evseId) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return evse, nil 24 | } 25 | 26 | func (m *managerImpl) Reserve(evseId int, connectorId *int, reservationId int, tagId string) error { 27 | logInfo := m.logger.WithFields(log.Fields{ 28 | "evseId": evseId, 29 | "tagId": tagId, 30 | "reservationId": reservationId, 31 | }) 32 | logInfo.Debugf("Reserving evse") 33 | 34 | evse, err := m.GetEVSE(evseId) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | evse.SetStatus(core.ChargePointStatusReserved, core.NoError) 40 | return nil 41 | } 42 | 43 | func (m *managerImpl) RemoveReservation(reservationId int) error { 44 | logInfo := m.logger.WithField("reservationId", reservationId) 45 | logInfo.Debugf("Removing reservation") 46 | 47 | _, isFound := m.reservations[reservationId] 48 | if !isFound { 49 | return ErrReservationNotFound 50 | } 51 | 52 | evse, err := m.GetEVSEWithReservationId(reservationId) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | m.reservations[reservationId] = nil 58 | evse.SetStatus(core.ChargePointStatusAvailable, core.NoError) 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/evse/manager/manager_test.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type managerTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (s *managerTestSuite) SetupTest() { 15 | } 16 | 17 | func (s *managerTestSuite) TestFindEVSE() { 18 | } 19 | 20 | func (s *managerTestSuite) TestFindAvailableEVSE() { 21 | } 22 | 23 | func (s *managerTestSuite) TestFindEVSEWithReservationId() { 24 | } 25 | 26 | func (s *managerTestSuite) TestFindEVSEWithTransactionId() { 27 | } 28 | 29 | func (s *managerTestSuite) TestFindEVSEWithTagId() { 30 | } 31 | 32 | func (s *managerTestSuite) TestStartCharging() { 33 | } 34 | 35 | func (s *managerTestSuite) TestStopCharging() { 36 | } 37 | 38 | func (s *managerTestSuite) TestStopAllEVSEs() { 39 | } 40 | 41 | func (s *managerTestSuite) TestRestoreEVSEs() { 42 | } 43 | 44 | func TestManager(t *testing.T) { 45 | log.SetLevel(log.DebugLevel) 46 | suite.Run(t, new(managerTestSuite)) 47 | } 48 | -------------------------------------------------------------------------------- /internal/evse/status.go: -------------------------------------------------------------------------------- 1 | package evse 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/notifications" 5 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func (evse *Impl) IsAvailable() bool { 10 | evse.mu.Lock() 11 | defer evse.mu.Unlock() 12 | return evse.status == core.ChargePointStatusAvailable && evse.availability == core.AvailabilityTypeOperative 13 | } 14 | 15 | func (evse *Impl) IsCharging() bool { 16 | evse.mu.Lock() 17 | defer evse.mu.Unlock() 18 | return evse.status == core.ChargePointStatusCharging 19 | } 20 | 21 | func (evse *Impl) IsPreparing() bool { 22 | evse.mu.Lock() 23 | defer evse.mu.Unlock() 24 | return evse.status == core.ChargePointStatusPreparing 25 | } 26 | 27 | func (evse *Impl) IsReserved() bool { 28 | evse.mu.Lock() 29 | defer evse.mu.Unlock() 30 | return evse.status == core.ChargePointStatusReserved 31 | } 32 | 33 | func (evse *Impl) IsUnavailable() bool { 34 | evse.mu.Lock() 35 | defer evse.mu.Unlock() 36 | return evse.status == core.ChargePointStatusUnavailable 37 | } 38 | 39 | func (evse *Impl) SetAvailability(isAvailable bool) { 40 | if isAvailable { 41 | evse.availability = core.AvailabilityTypeOperative 42 | return 43 | } 44 | 45 | evse.availability = core.AvailabilityTypeInoperative 46 | } 47 | 48 | func (evse *Impl) SetStatus(status core.ChargePointStatus, errCode core.ChargePointErrorCode) { 49 | logInfo := evse.logger.WithFields(log.Fields{ 50 | "status": status, 51 | "err": errCode, 52 | }) 53 | logInfo.Debugf("Setting evse status %s with err %s", status, errCode) 54 | 55 | evse.mu.Lock() 56 | defer evse.mu.Unlock() 57 | 58 | evse.status = status 59 | evse.errorCode = errCode 60 | 61 | // Notify the channel that a status was updated 62 | if evse.notificationChannel != nil { 63 | logInfo.Debug("Sending status notification") 64 | evse.notificationChannel <- notifications.NewStatusNotification(evse.evseId, string(status), string(errCode)) 65 | } 66 | } 67 | 68 | func (evse *Impl) GetStatus() (core.ChargePointStatus, core.ChargePointErrorCode) { 69 | return evse.status, evse.errorCode 70 | } 71 | -------------------------------------------------------------------------------- /internal/pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 8 | "github.com/dgraph-io/badger/v3" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | db *badger.DB 14 | once = sync.Once{} 15 | ) 16 | 17 | func Get() *badger.DB { 18 | once.Do(func() { 19 | opts := badger.DefaultOptions(settings.DatabasePath) 20 | opts.Logger = newLogger() 21 | opts.NumGoroutines = 3 22 | 23 | // Load/initialize a database for EVSE, tags, users and settings 24 | badgerDb, err := badger.Open(opts) 25 | if err != nil { 26 | log.WithError(err).Panic("Cannot open/create database") 27 | } 28 | 29 | db = badgerDb 30 | 31 | // Migrate the database to the latest version 32 | migration(db) 33 | }) 34 | 35 | return db 36 | } 37 | 38 | func GetEvseSettings(db *badger.DB) []settings.EVSE { 39 | var evseSettings []settings.EVSE 40 | 41 | // Query the database for EVSE settings. 42 | err := db.View(func(txn *badger.Txn) error { 43 | it := txn.NewIterator(badger.DefaultIteratorOptions) 44 | defer it.Close() 45 | 46 | prefix := []byte("evse-") 47 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 48 | var data settings.EVSE 49 | item := it.Item() 50 | 51 | // Value should be the EVSE struct. 52 | err := item.Value(func(v []byte) error { 53 | return json.Unmarshal(v, &data) 54 | }) 55 | if err != nil { 56 | log.WithError(err).Warnf("Error unmarshalling EVSE settings for %s", item.Key()) 57 | continue 58 | } 59 | 60 | evseSettings = append(evseSettings, data) 61 | } 62 | 63 | return txn.Commit() 64 | }) 65 | if err != nil { 66 | log.WithError(err).Error("Error querying for EVSE settings") 67 | } 68 | 69 | return evseSettings 70 | } 71 | -------------------------------------------------------------------------------- /internal/pkg/database/keys.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChargePi/ChargePi-go/pkg/models/ocpp" 7 | ) 8 | 9 | func GetEvseKey(evseId int) string { 10 | return fmt.Sprintf("evse-%d", evseId) 11 | } 12 | 13 | func GetLocalAuthTagPrefix(tagId string) []byte { 14 | return []byte(fmt.Sprintf("auth-tag-%s", tagId)) 15 | } 16 | 17 | func GetLocalAuthVersion() []byte { 18 | return []byte("auth-version") 19 | } 20 | 21 | func GetSmartChargingProfile(profileId int) []byte { 22 | return []byte(fmt.Sprintf("profile-%d", profileId)) 23 | } 24 | 25 | func GetOcppConfigurationKey(version ocpp.ProtocolVersion) []byte { 26 | return []byte(fmt.Sprintf("ocpp-configuration-%s", version)) 27 | } 28 | 29 | func GetSettingsKey() []byte { 30 | return []byte(fmt.Sprintf("settings")) 31 | } 32 | -------------------------------------------------------------------------------- /internal/pkg/database/logger.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import log "github.com/sirupsen/logrus" 4 | 5 | type Logger struct { 6 | logger log.FieldLogger 7 | } 8 | 9 | func newLogger() *Logger { 10 | logger := log.StandardLogger().WithField("component", "database") 11 | return &Logger{ 12 | logger: logger, 13 | } 14 | } 15 | 16 | func (l *Logger) Errorf(s string, i ...interface{}) { 17 | l.logger.Errorf(s, i) 18 | } 19 | 20 | func (l *Logger) Warningf(s string, i ...interface{}) { 21 | l.logger.Warningf(s, i) 22 | } 23 | 24 | func (l *Logger) Infof(s string, i ...interface{}) { 25 | l.logger.Infof(s, i) 26 | } 27 | 28 | func (l *Logger) Debugf(s string, i ...interface{}) { 29 | l.logger.Debugf(s, i) 30 | } 31 | -------------------------------------------------------------------------------- /internal/pkg/database/migration.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | userDatabase "github.com/ChargePi/ChargePi-go/internal/users/pkg/database" 5 | "github.com/ChargePi/ChargePi-go/internal/users/pkg/models" 6 | "github.com/dgraph-io/badger/v3" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Initialize the database with default settings. 11 | func migration(db *badger.DB) { 12 | log.Debug("Migrating database") 13 | 14 | userDb := userDatabase.NewUserDb(db) 15 | _ = userDb.AddUser(models.User{ 16 | Username: "manufacturer", 17 | Password: "manufacturer", 18 | Role: string(models.Manufacturer), 19 | }) 20 | 21 | _ = userDb.AddUser(models.User{ 22 | Username: "technician", 23 | Password: "technician", 24 | Role: string(models.Technician), 25 | }) 26 | 27 | _ = userDb.AddUser(models.User{ 28 | Username: "observer", 29 | Password: "observer", 30 | Role: string(models.Observer), 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/pkg/models/charge-point/consts.go: -------------------------------------------------------------------------------- 1 | package chargePoint 2 | 3 | const FirmwareVersion = "v1.0.0" 4 | -------------------------------------------------------------------------------- /internal/pkg/models/charge-point/model.go: -------------------------------------------------------------------------------- 1 | package chargePoint 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/notifications" 8 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 9 | "github.com/ChargePi/ChargePi-go/pkg/display" 10 | "github.com/ChargePi/ChargePi-go/pkg/indicator" 11 | data "github.com/ChargePi/ChargePi-go/pkg/models/ocpp" 12 | settings2 "github.com/ChargePi/ChargePi-go/pkg/models/settings" 13 | "github.com/ChargePi/ChargePi-go/pkg/reader" 14 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" 15 | ocppDisplay "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | var ( 20 | ErrConnectorNil = errors.New("connector pointer is nil") 21 | ErrChargePointUnavailable = errors.New("charge point unavailable") 22 | ErrTagUnauthorized = errors.New("tag unauthorized") 23 | ) 24 | 25 | type ChargePoint interface { 26 | // Lifecycle APIs 27 | Connect(ctx context.Context, serverUrl string) 28 | CleanUp(reason core.Reason) 29 | Reset(resetType string) error 30 | ApplyOpts(opts ...Options) 31 | 32 | // Core functionality 33 | StartCharging(evseId, connectorId int, tagId string) error 34 | StopCharging(evseId, connectorId int, reason core.Reason) error 35 | StartChargingFreeMode(evseId int) error 36 | 37 | // Connector APIs 38 | SendEVSEsDetails(evseId int, maxPower float32, connectors ...data.Connector) 39 | ListenForConnectorStatusChange(ctx context.Context, ch <-chan notifications.StatusNotification) 40 | 41 | // Options 42 | SetLogger(logger log.FieldLogger) 43 | 44 | // Display APIs 45 | SetDisplay(display display.Display) error 46 | DisplayMessage(display ocppDisplay.MessageInfo) error 47 | 48 | // Indicator APIs 49 | SetIndicator(indicator indicator.Indicator) error 50 | SetIndicatorSettings(settings settings2.IndicatorStatusMapping) error 51 | GetIndicatorSettings() settings2.IndicatorStatusMapping 52 | 53 | // Reader 54 | SetReader(reader reader.Reader) error 55 | ListenForTag(ctx context.Context, tagChannel <-chan string) (*string, error) 56 | 57 | // Settings 58 | SetSettings(settings settings.Info) error 59 | GetSettings() settings.Info 60 | 61 | // Connection settings 62 | SetConnectionSettings(settings settings.ConnectionSettings) error 63 | GetConnectionSettings() settings.ConnectionSettings 64 | 65 | GetVersion() string 66 | GetStatus() string 67 | // SetStatus(status string) error 68 | IsConnected() bool 69 | } 70 | -------------------------------------------------------------------------------- /internal/pkg/models/charge-point/opts.go: -------------------------------------------------------------------------------- 1 | package chargePoint 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/ChargePi/ChargePi-go/pkg/display" 8 | "github.com/ChargePi/ChargePi-go/pkg/indicator" 9 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 10 | "github.com/ChargePi/ChargePi-go/pkg/reader" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type Options func(point ChargePoint) 15 | 16 | // WithLogger add logger to the ChargePoint 17 | func WithLogger(logger log.FieldLogger) Options { 18 | return func(point ChargePoint) { 19 | if logger != nil { 20 | point.SetLogger(logger) 21 | } 22 | } 23 | } 24 | 25 | // WithReaderFromSettings creates a TagReader based on the settings. 26 | func WithReaderFromSettings(ctx context.Context, readerSettings settings.TagReader) Options { 27 | return func(point ChargePoint) { 28 | // Create reader based on settings 29 | tagReader, err := reader.NewTagReader(readerSettings) 30 | switch { 31 | case errors.Is(err, reader.ErrReaderDisabled): 32 | return 33 | case errors.Is(err, reader.ErrReaderUnsupported): 34 | log.WithError(err).Fatal("Error attaching a display") 35 | } 36 | 37 | point.SetReader(tagReader) 38 | } 39 | } 40 | 41 | // WithReader adds the reader to the charge point and starts listening to the Reader. 42 | func WithReader(ctx context.Context, tagReader reader.Reader) Options { 43 | return func(point ChargePoint) { 44 | point.SetReader(tagReader) 45 | } 46 | } 47 | 48 | // WithDisplayFromSettings create a Display based on the provided settings. 49 | func WithDisplayFromSettings(lcdSettings settings.Display) Options { 50 | return func(point ChargePoint) { 51 | lcd, err := display.NewDisplay(lcdSettings) 52 | switch { 53 | case errors.Is(err, display.ErrDisplayDisabled): 54 | return 55 | case errors.Is(err, display.ErrDisplayUnsupported), errors.Is(err, display.ErrInvalidConnectionDetails): 56 | log.WithError(err).Fatal("Error attaching a display") 57 | } 58 | 59 | point.SetDisplay(lcd) 60 | } 61 | } 62 | 63 | // WithDisplay add the provided Display to the ChargePoint. 64 | func WithDisplay(display display.Display) Options { 65 | return func(point ChargePoint) { 66 | point.SetDisplay(display) 67 | } 68 | } 69 | 70 | // WithIndicator add an indicator 71 | func WithIndicator(indicator indicator.Indicator) Options { 72 | return func(point ChargePoint) { 73 | point.SetIndicator(indicator) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/pkg/models/notifications/meter-values.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 5 | ) 6 | 7 | type MeterValueNotification struct { 8 | ConnectorId *int 9 | EvseId int 10 | TransactionId *int 11 | MeterValues []types.MeterValue 12 | } 13 | 14 | func NewMeterValueNotification(evseId int, connectorId, transactionId *int, meterValues ...types.MeterValue) MeterValueNotification { 15 | return MeterValueNotification{ 16 | ConnectorId: connectorId, 17 | EvseId: evseId, 18 | TransactionId: transactionId, 19 | MeterValues: meterValues, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/pkg/models/notifications/status.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | type StatusNotification struct { 4 | EvseId int 5 | Status string 6 | ErrorCode string 7 | } 8 | 9 | func NewStatusNotification(evseId int, status, errorCode string) StatusNotification { 10 | return StatusNotification{ 11 | Status: status, 12 | EvseId: evseId, 13 | ErrorCode: errorCode, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/auth.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" 5 | ) 6 | 7 | type AuthList struct { 8 | Version int `json:"version" yaml:"version" mapstructure:"version" validate:"min=1"` 9 | Tags []localauth.AuthorizationData `json:"tags" yaml:"tags" mapstructure:"tags" validate:"min=1"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/charge-point.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/pkg/models/ocpp" 5 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 6 | ) 7 | 8 | type ( 9 | Settings struct { 10 | ChargePoint ChargePoint `json:"chargePoint" yaml:"chargePoint" mapstructure:"chargePoint"` 11 | Api Api `json:"api" yaml:"api" mapstructure:"api"` 12 | Ui Ui `json:"ui" yaml:"ui" mapstructure:"ui"` 13 | } 14 | 15 | ChargePoint struct { 16 | Info Info `json:"info" yaml:"info" mapstructure:"info"` 17 | ConnectionSettings ConnectionSettings `json:"connectionSettings" yaml:"connectionSettings" mapstructure:"connectionSettings"` 18 | Logging Logging `json:"logging" yaml:"logging" mapstructure:"logging"` 19 | Hardware Hardware `json:"hardware" yaml:"hardware" mapstructure:"hardware"` 20 | } 21 | 22 | Hardware struct { 23 | Display settings.Display `json:"display" yaml:"display" mapstructure:"display"` 24 | TagReader settings.TagReader `json:"reader" yaml:"reader" mapstructure:"reader"` 25 | Indicator settings.Indicator `json:"indicator" yaml:"indicator" mapstructure:"indicator"` 26 | } 27 | 28 | ConnectionSettings struct { 29 | Id string `json:"id,omitempty" yaml:"id" mapstructure:"id" validate:"required"` 30 | ProtocolVersion ocpp.ProtocolVersion `json:"protocolVersion,omitempty" yaml:"protocolVersion" mapstructure:"protocolVersion" validate:"required"` 31 | ServerUri string `json:"uri,omitempty" yaml:"uri" mapstructure:"uri" validate:"required"` 32 | BasicAuthUsername string `json:"basicAuthUser,omitempty" yaml:"basicAuthUser" mapstructure:"basicAuthUser"` 33 | BasicAuthPassword string `json:"basicAuthPass,omitempty" yaml:"basicAuthPass" mapstructure:"basicAuthPass"` 34 | TLS TLS `json:"tls" yaml:"tls" mapstructure:"tls"` 35 | } 36 | 37 | Info struct { 38 | // Maximum time allowed if free mode is enabled 39 | MaxChargingTime *int `json:"MaxChargingTime,omitempty" yaml:"MaxChargingTime" mapstructure:"MaxChargingTime"` 40 | FreeMode bool `json:"freeMode,omitempty" yaml:"freeMode" mapstructure:"freeMode"` 41 | // AC or DC 42 | Type string `json:"type,omitempty" yaml:"type" mapstructure:"type" validate:"oneof=AC DC"` 43 | // in kW 44 | MaxPower float32 `json:"maxPower,omitempty" yaml:"maxPower" mapstructure:"maxPower"` 45 | OCPPDetails OCPPDetails `json:"ocpp" yaml:"ocpp" mapstructure:"ocpp"` 46 | } 47 | 48 | FreeChargingMode struct { 49 | Enabled bool `json:"enabled,omitempty" yaml:"enabled" mapstructure:"enabled"` 50 | Strategy string `json:"strategy,omitempty" yaml:"strategy" mapstructure:"strategy"` 51 | } 52 | ) 53 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/common.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | TLS struct { 5 | IsEnabled bool `json:"enabled,omitempty" yaml:"enabled" mapstructure:"enabled"` 6 | CACertificatePath string `json:"CACertificatePath,omitempty" yaml:"CACertificatePath" mapstructure:"CACertificatePath"` 7 | ClientCertificatePath string `json:"certificatePath,omitempty" yaml:"certificatePath" mapstructure:"certificatePath"` 8 | PrivateKeyPath string `json:"keyPath,omitempty" yaml:"keyPath" mapstructure:"keyPath"` 9 | } 10 | 11 | Logging struct { 12 | LogTypes []LogType `json:"logTypes,omitempty" yaml:"logTypes" mapstructure:"logTypes"` 13 | } 14 | 15 | LogType struct { 16 | Type string `json:"type,omitempty" yaml:"type" mapstructure:"type" validate:"required"` // remote, console 17 | Format *string `json:"format,omitempty" yaml:"format" mapstructure:"format"` // gelf, syslog, json, etc 18 | Address *string `json:"address,omitempty" yaml:"address" mapstructure:"address"` 19 | } 20 | 21 | Api struct { 22 | Enabled bool `json:"enabled,omitempty" yaml:"enabled" mapstructure:"enabled"` 23 | Address string `json:"address,omitempty" yaml:"address" mapstructure:"address"` 24 | TLS TLS `json:"tls,omitempty" yaml:"tls" mapstructure:"tls"` 25 | } 26 | 27 | Ui struct { 28 | Enabled bool `json:"enabled,omitempty" yaml:"enabled" mapstructure:"enabled"` 29 | Address string `json:"address,omitempty" yaml:"address" mapstructure:"address"` 30 | TLS TLS `json:"tls,omitempty" yaml:"tls" mapstructure:"tls"` 31 | } 32 | ) 33 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/consts.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | const DatabasePath = "/tmp/chargepi" 4 | 5 | const ( 6 | CurrentFolder = "./configs" 7 | EvseFolder = "./configs/evses" 8 | DockerFolder = "/etc/ChargePi/configs" 9 | ) 10 | 11 | // Configuration variables 12 | const ( 13 | Model = "chargepoint.info.ocpp.model" 14 | Vendor = "chargepoint.info.ocpp.vendor" 15 | MaxChargingTime = "chargepoint.info.maxChargingTime" 16 | ProtocolVersion = "chargepoint.info.protocolVersion" 17 | 18 | Debug = "debug" 19 | ApiEnabled = "api.enabled" 20 | ApiAddress = "api.address" 21 | ApiPort = "api.port" 22 | ) 23 | 24 | // Flags 25 | const ( 26 | DebugFlag = "debug" 27 | ApiAddressFlag = "api-address" 28 | SettingsFlag = "settings" 29 | EvseFlag = "evse" 30 | AuthFileFlag = "auth" 31 | OcppConfigPathFlag = "ocpp" 32 | OcppVersion = "v" 33 | ) 34 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/evse.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 5 | ) 6 | 7 | type ( 8 | EVSE struct { 9 | EvseId int `json:"evseId,omitempty" yaml:"evseId" mapstructure:"evseId" validate:"required,gte=1"` 10 | MaxPower float32 `json:"maxPower" yaml:"maxPower" mapstructure:"maxPower" validate:"gt=0"` 11 | EVCC settings.EVCC `json:"evcc" yaml:"evcc" mapstructure:"evcc" validate:"required"` 12 | PowerMeter settings.PowerMeter `json:"powerMeter" yaml:"powerMeter" mapstructure:"powerMeter"` 13 | Connectors []Connector `json:"connectors" yaml:"connectors" mapstructure:"connectors"` 14 | } 15 | 16 | Connector struct { 17 | ConnectorId int `json:"connectorId,omitempty" yaml:"connectorId" mapstructure:"connectorId"` 18 | Type string `json:"type,omitempty" yaml:"type" mapstructure:"type"` 19 | Status string `json:"status,omitempty" yaml:"status" mapstructure:"status"` 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /internal/pkg/models/settings/ocpp.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | OCPPDetails struct { 5 | Vendor string `json:"vendor" yaml:"vendor" mapstructure:"vendor" validate:"required"` 6 | Model string `json:"model" yaml:"model" mapstructure:"model" validate:"required"` 7 | ChargeBoxSerialNumber string `json:"chargeBoxSerialNumber,omitempty" yaml:"chargeBoxSerialNumber,omitempty" mapstructure:"chargeBoxSerialNumber,omitempty"` 8 | ChargePointSerialNumber string `json:"pointSerialNumber" yaml:"pointSerialNumber" mapstructure:"pointSerialNumber"` 9 | Iccid string `json:"iccid,omitempty" yaml:"iccid,omitempty" mapstructure:"iccid"` 10 | Imsi string `json:"imsi,omitempty" yaml:"imsi,omitempty" mapstructure:"imsi"` 11 | } 12 | ) 13 | 14 | const ( 15 | ISO15118PnCEnabledConfigurationKey = "ISO15118PnCEnabled" 16 | ) 17 | -------------------------------------------------------------------------------- /internal/pkg/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-co-op/gocron" 7 | ) 8 | 9 | func NewScheduler() *gocron.Scheduler { 10 | scheduler := gocron.NewScheduler(time.UTC) 11 | // Set to execute jobs on first interval and not immediately 12 | scheduler.WaitForScheduleAll() 13 | // Start non-blocking 14 | scheduler.StartAsync() 15 | 16 | return scheduler 17 | } 18 | -------------------------------------------------------------------------------- /internal/pkg/settings/exporter_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type exporterTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *exporterTestSuite) SetupTest() { 14 | } 15 | 16 | func (s *exporterTestSuite) TestExportEVSESettings() { 17 | } 18 | 19 | func (s *exporterTestSuite) TestExportOcppConfiguration() { 20 | } 21 | 22 | func (s *exporterTestSuite) TestExportLocalAuthList() { 23 | } 24 | 25 | func TestExporter(t *testing.T) { 26 | suite.Run(t, new(exporterTestSuite)) 27 | } 28 | -------------------------------------------------------------------------------- /internal/pkg/settings/importer_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type importerTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *importerTestSuite) SetupTest() { 14 | } 15 | 16 | func (s *importerTestSuite) TestImportEVSESettings() { 17 | } 18 | 19 | func (s *importerTestSuite) TestImportOcppConfiguration() { 20 | } 21 | 22 | func (s *importerTestSuite) TestImportLocalAuthList() { 23 | } 24 | 25 | func TestImporter(t *testing.T) { 26 | suite.Run(t, new(importerTestSuite)) 27 | } 28 | -------------------------------------------------------------------------------- /internal/pkg/settings/manager_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type managerTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *managerTestSuite) SetupTest() { 14 | } 15 | 16 | func TestManager(t *testing.T) { 17 | suite.Run(t, new(managerTestSuite)) 18 | } 19 | -------------------------------------------------------------------------------- /internal/pkg/util/helpers.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // IsNilInterfaceOrPointer check if the variable is nil or if the pointer's value is nil. 8 | func IsNilInterfaceOrPointer(sth interface{}) bool { 9 | return sth == nil || (reflect.ValueOf(sth).Kind() == reflect.Ptr && reflect.ValueOf(sth).IsNil()) 10 | } 11 | -------------------------------------------------------------------------------- /internal/pkg/util/ocpp.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 12 | "github.com/agrison/go-commons-lang/stringUtils" 13 | "github.com/lorenzodonini/ocpp-go/ws" 14 | "github.com/pkg/errors" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // CreateConnectionUrl creates a connection url from the provided settings 19 | func CreateConnectionUrl(connectionSettings settings.ConnectionSettings) string { 20 | var ( 21 | serverUrl = fmt.Sprintf("ws://%s", connectionSettings.ServerUri) 22 | ) 23 | 24 | // Replace insecure Websockets 25 | if connectionSettings.TLS.IsEnabled { 26 | serverUrl = strings.Replace(serverUrl, "ws", "wss", 1) 27 | } 28 | 29 | return serverUrl 30 | } 31 | 32 | // CreateClient creates a Websocket client based on the settings. 33 | func CreateClient(connectionSettings settings.ConnectionSettings, pingInterval *string) (*ws.Client, error) { 34 | log.Debug("Creating a websocket client") 35 | 36 | client := ws.NewClient() 37 | clientConfig := ws.NewClientTimeoutConfig() 38 | 39 | if pingInterval != nil { 40 | // Set the ping interval 41 | duration, err := time.ParseDuration(fmt.Sprintf("%ss", *pingInterval)) 42 | if err == nil { 43 | clientConfig.PingPeriod = duration 44 | } 45 | } 46 | 47 | // Check if the TLS is enabled for the client 48 | if connectionSettings.TLS.IsEnabled { 49 | log.Debug("TLS enabled for the websocket client") 50 | 51 | certPool, err := x509.SystemCertPool() 52 | if err != nil { 53 | return nil, errors.Wrap(err, "Cannot fetch certificate pool") 54 | } 55 | 56 | // Load CA cert 57 | caCert, err := ioutil.ReadFile(connectionSettings.TLS.CACertificatePath) 58 | if err != nil { 59 | return nil, errors.Wrap(err, "error reading CA certificate") 60 | } else if !certPool.AppendCertsFromPEM(caCert) { 61 | return nil, errors.Wrap(err, "no ca.cert file found, will use system CA certificates") 62 | } 63 | 64 | // Load client certificate 65 | certificate, err := tls.LoadX509KeyPair(connectionSettings.TLS.ClientCertificatePath, connectionSettings.TLS.PrivateKeyPath) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "couldn't load client TLS certificate") 68 | } 69 | 70 | // Create client with TLS config 71 | client = ws.NewTLSClient(&tls.Config{ 72 | RootCAs: certPool, 73 | Certificates: []tls.Certificate{certificate}, 74 | }) 75 | } 76 | 77 | // If HTTP basic auth is provided, set it in the Websocket client 78 | if stringUtils.IsNoneEmpty(connectionSettings.BasicAuthUsername, connectionSettings.BasicAuthPassword) { 79 | log.Debug("Basic auth enabled") 80 | client.SetBasicAuth(connectionSettings.BasicAuthUsername, connectionSettings.BasicAuthPassword) 81 | } 82 | 83 | client.SetTimeoutConfig(clientConfig) 84 | return client, nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/sessions/pkg/database/session-badger_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | // Create a test for the session repository 4 | 5 | import ( 6 | "testing" 7 | 8 | session "github.com/ChargePi/ChargePi-go/internal/sessions/pkg/models" 9 | "github.com/dgraph-io/badger/v3" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSessionRepository(t *testing.T) { 14 | db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true)) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer db.Close() 19 | 20 | // Create a session repository 21 | repo := NewSessionBadgerDb(db) 22 | 23 | // Create a session 24 | newSession := session.NewEmptySession() 25 | err = newSession.StartSession("1", "tag-id") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | // Create the session 31 | err = repo.CreateSession(newSession) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | // Get the session 37 | s, err := repo.GetSessionWithTransactionId("1") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | // Check that the session is the same 43 | assert.Equal(t, newSession, s, "session should be the same") 44 | 45 | // End the session 46 | err = repo.StopSession("1") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | // Get the session 52 | s, err = repo.GetSessionWithTransactionId("transaction-id") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | // Check that the session is ended 58 | assert.True(t, s.IsActive, "session should be ended") 59 | } 60 | -------------------------------------------------------------------------------- /internal/sessions/pkg/database/session.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | session "github.com/ChargePi/ChargePi-go/internal/sessions/pkg/models" 5 | ) 6 | 7 | type SessionRepository interface { 8 | CreateSession(session *session.Session) error 9 | StopSession(transactionId string) error 10 | UpdateSession(session *session.Session) error 11 | GetSession(evseId int, connectorId *int) (*session.Session, error) 12 | GetSessions() ([]session.Session, error) 13 | GetActiveSessions() ([]session.Session, error) 14 | GetSessionWithTransactionId(transactionId string) (*session.Session, error) 15 | GetSessionWithTagId(tagId string) (*session.Session, error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/sessions/service/session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type sessionServiceTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *sessionServiceTestSuite) SetupTest() { 14 | 15 | } 16 | 17 | func TestSession(t *testing.T) { 18 | suite.Run(t, new(sessionServiceTestSuite)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/smart-charging/composite-schedule.go: -------------------------------------------------------------------------------- 1 | package smartCharging 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 8 | ) 9 | 10 | type ScheduleInterval struct { 11 | StartTime time.Time 12 | Duration time.Duration 13 | Limit float64 14 | } 15 | 16 | func CreateCompositeSchedule(connectorSchedules []*types.ChargingProfile) []ScheduleInterval { 17 | // Map to store the minimum limit for each time interval 18 | minLimitMap := make(map[time.Time]float64) 19 | 20 | // Loop through all the charging profiles for the connector 21 | for _, profile := range connectorSchedules { 22 | if profile == nil { 23 | continue 24 | } 25 | 26 | if profile.ValidFrom.After(time.Now()) || profile.ValidTo.Before(time.Now()) { 27 | continue 28 | } 29 | 30 | startTime := time.Now() 31 | 32 | // Loop through all the schedule intervals in the profile 33 | for _, period := range profile.ChargingSchedule.ChargingSchedulePeriod { 34 | endTime := startTime.Add(time.Duration(period.StartPeriod)) 35 | 36 | // Check if the current period limit is less than the stored minimum limit 37 | if minLimit, exists := minLimitMap[startTime]; exists { 38 | if period.Limit < minLimit { 39 | minLimitMap[startTime] = period.Limit 40 | } 41 | } else { 42 | minLimitMap[startTime] = period.Limit 43 | } 44 | 45 | // Repeat the same process for the end time of the period 46 | if minLimit, exists := minLimitMap[endTime]; exists { 47 | if period.Limit < minLimit { 48 | minLimitMap[endTime] = period.Limit 49 | } 50 | } else { 51 | minLimitMap[endTime] = period.Limit 52 | } 53 | } 54 | } 55 | 56 | return toScheduleInterval(minLimitMap) 57 | } 58 | 59 | // Convert the map to a slice of schedule intervals 60 | func toScheduleInterval(minLimitMap map[time.Time]float64) []ScheduleInterval { 61 | var compositeSchedule []ScheduleInterval 62 | var startTimes []time.Time 63 | 64 | for startTime := range minLimitMap { 65 | startTimes = append(startTimes, startTime) 66 | } 67 | sort.Slice(startTimes, func(i, j int) bool { return startTimes[i].Before(startTimes[j]) }) 68 | 69 | for i := 0; i < len(startTimes)-1; i++ { 70 | startTime := startTimes[i] 71 | endTime := startTimes[i+1] 72 | limit := minLimitMap[startTime] 73 | duration := endTime.Sub(startTime) 74 | compositeSchedule = append(compositeSchedule, ScheduleInterval{ 75 | StartTime: startTime, 76 | Duration: duration, 77 | Limit: limit, 78 | }) 79 | } 80 | 81 | return compositeSchedule 82 | } 83 | -------------------------------------------------------------------------------- /internal/smart-charging/helpers.go: -------------------------------------------------------------------------------- 1 | package smartCharging 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 7 | ) 8 | 9 | func getValidProfiles(profiles []types.ChargingProfile) []types.ChargingProfile { 10 | ret := []types.ChargingProfile{} 11 | 12 | for _, profile := range profiles { 13 | 14 | // Exception is TxProfile 15 | if profile.ChargingProfilePurpose == types.ChargingProfilePurposeTxProfile { 16 | ret = append(ret, profile) 17 | continue 18 | } 19 | 20 | // Valid if the validity dates are not set 21 | if profile.ValidFrom == nil && profile.ValidTo == nil { 22 | ret = append(ret, profile) 23 | continue 24 | } 25 | 26 | // Check if the date hasn't expired yet first 27 | if profile.ValidTo != nil && time.Now().Before(profile.ValidTo.Time) { 28 | ret = append(ret, profile) 29 | continue 30 | } 31 | 32 | // Check if it is even valid 33 | if profile.ValidFrom != nil && time.Now().After(profile.ValidFrom.Time) { 34 | ret = append(ret, profile) 35 | continue 36 | } 37 | } 38 | 39 | return ret 40 | } 41 | 42 | func getProfileWithHighestStack(profiles []types.ChargingProfile) *types.ChargingProfile { 43 | var ret *types.ChargingProfile 44 | 45 | switch len(profiles) { 46 | case 0: 47 | return nil 48 | case 1: 49 | return &profiles[0] 50 | } 51 | 52 | maxStackLevel := profiles[0].StackLevel 53 | 54 | for _, profile := range profiles[1:] { 55 | if profile.StackLevel > maxStackLevel { 56 | maxStackLevel = profile.StackLevel 57 | ret = &profile 58 | } 59 | } 60 | 61 | return ret 62 | } 63 | 64 | func getProfilesWithPurpose(purpose types.ChargingProfilePurposeType, profiles []types.ChargingProfile) []types.ChargingProfile { 65 | ret := []types.ChargingProfile{} 66 | 67 | for _, profile := range profiles { 68 | if profile.ChargingProfilePurpose == purpose { 69 | ret = append(ret, profile) 70 | } 71 | } 72 | 73 | return ret 74 | } 75 | -------------------------------------------------------------------------------- /internal/smart-charging/helpers_test.go: -------------------------------------------------------------------------------- 1 | package smartCharging 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type helpersTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *helpersTestSuite) SetupTest() { 14 | } 15 | 16 | func (s *helpersTestSuite) TestRestoreState() { 17 | } 18 | 19 | func (s *helpersTestSuite) TestNotifyConnectorStatus() { 20 | } 21 | 22 | func TestHelpers(t *testing.T) { 23 | suite.Run(t, new(helpersTestSuite)) 24 | } 25 | -------------------------------------------------------------------------------- /internal/smart-charging/manager_test.go: -------------------------------------------------------------------------------- 1 | package smartCharging 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type smartChargingManagerTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func (s *smartChargingManagerTestSuite) SetupTest() { 14 | } 15 | 16 | func (s *smartChargingManagerTestSuite) TestRestoreState() { 17 | } 18 | 19 | func (s *smartChargingManagerTestSuite) TestNotifyConnectorStatus() { 20 | } 21 | 22 | func TestSmartChargingManager(t *testing.T) { 23 | suite.Run(t, new(smartChargingManagerTestSuite)) 24 | } 25 | -------------------------------------------------------------------------------- /internal/users/pkg/database/badger_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | -------------------------------------------------------------------------------- /internal/users/pkg/database/interface.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/ChargePi/ChargePi-go/internal/users/pkg/models" 4 | 5 | type Database interface { 6 | GetUser(username string) (*models.User, error) 7 | GetUsers() []models.User 8 | AddUser(user models.User) error 9 | UpdateUser(models.User) (*models.User, error) 10 | DeleteUser(username string) error 11 | } 12 | -------------------------------------------------------------------------------- /internal/users/pkg/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Username string `json:"username" mapstructure:"username"` 5 | Password string `json:"password" mapstructure:"password"` 6 | Role string `json:"role" mapstructure:"role"` 7 | } 8 | 9 | type Role string 10 | 11 | const ( 12 | Manufacturer = Role("Manufacturer") 13 | Technician = Role("Technician") 14 | Observer = Role("Observer") 15 | ) 16 | -------------------------------------------------------------------------------- /internal/users/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/users/pkg/database" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type serviceTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (s *serviceTestSuite) SetupTest() { 16 | } 17 | 18 | func (s *serviceTestSuite) TestGetUsers() { 19 | dbMock := database.NewDatabaseMock(s.T()) 20 | service := NewUserService(dbMock) 21 | 22 | user, err := service.GetUser("") 23 | s.Assert().NoError(err) 24 | s.Assert().Equal("", user.Username) 25 | } 26 | 27 | func (s *serviceTestSuite) TestAddUser() { 28 | dbMock := database.NewDatabaseMock(s.T()) 29 | service := NewUserService(dbMock) 30 | 31 | user, err := service.GetUser("") 32 | s.Assert().NoError(err) 33 | s.Assert().Equal("", user.Username) 34 | } 35 | 36 | func (s *serviceTestSuite) TestGetUser() { 37 | dbMock := database.NewDatabaseMock(s.T()) 38 | service := NewUserService(dbMock) 39 | 40 | user, err := service.GetUser("") 41 | s.Assert().NoError(err) 42 | s.Assert().Equal("", user.Username) 43 | } 44 | 45 | func (s *serviceTestSuite) TestUpdateUser() { 46 | dbMock := database.NewDatabaseMock(s.T()) 47 | service := NewUserService(dbMock) 48 | 49 | user, err := service.GetUser("") 50 | s.Assert().NoError(err) 51 | s.Assert().Equal("", user.Username) 52 | } 53 | 54 | func TestService(t *testing.T) { 55 | log.SetLevel(log.DebugLevel) 56 | suite.Run(t, new(serviceTestSuite)) 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | proto: 2 | mkdir -p pkg/grpc 3 | protoc --go_out=./pkg/grpc --go_opt=paths=source_relative \ 4 | --proto_path=pkg/proto \ 5 | --go-grpc_out=./pkg/grpc --go-grpc_opt=paths=source_relative \ 6 | pkg/proto/*.proto 7 | 8 | install-dependencies: 9 | sudo sh ./scripts/install-dependencies.sh pn532_uart 0 -------------------------------------------------------------------------------- /pkg/display/display.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/pkg/util" 7 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 8 | "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const ( 13 | DriverHD44780 = "hd44780" 14 | TypeDummy = "dummy" 15 | ) 16 | 17 | var ( 18 | ErrDisplayUnsupported = errors.New("display type unsupported") 19 | ErrInvalidConnectionDetails = errors.New("connection details invalid or empty") 20 | ErrDisplayDisabled = errors.New("display disabled") 21 | ) 22 | 23 | // Display is an abstraction layer for concrete implementation of a display. 24 | type Display interface { 25 | DisplayMessage(message display.MessageInfo) 26 | // GetCurrentMessage(display.MessageInfo) (display.MessageStatus, error) 27 | // GetMessages(reqId int) ([]display.GetDisplayMessagesResponse, error) 28 | Clear() 29 | Cleanup() 30 | GetType() string 31 | } 32 | 33 | // NewDisplay returns a concrete implementation of a Display based on the drivers that are supported. 34 | // The Display is built with the settings from the settings file. 35 | func NewDisplay(lcdSettings settings.Display) (Display, error) { 36 | if lcdSettings.IsEnabled { 37 | log.Info("Preparing display from config") 38 | 39 | switch lcdSettings.Driver { 40 | case DriverHD44780: 41 | if util.IsNilInterfaceOrPointer(lcdSettings.HD44780) { 42 | return nil, ErrInvalidConnectionDetails 43 | } 44 | 45 | lcd, err := NewHD44780(*lcdSettings.HD44780) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return lcd, nil 51 | case TypeDummy: 52 | return NewDummy(lcdSettings.DisplayDummy) 53 | default: 54 | return nil, ErrDisplayUnsupported 55 | } 56 | } 57 | 58 | return nil, ErrDisplayDisabled 59 | } 60 | -------------------------------------------------------------------------------- /pkg/display/dummy.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 5 | "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type Dummy struct { 10 | logger *log.Logger 11 | settings settings.DisplayDummy 12 | } 13 | 14 | func NewDummy(settings *settings.DisplayDummy) (*Dummy, error) { 15 | return &Dummy{ 16 | settings: *settings, 17 | }, nil 18 | } 19 | 20 | func (d *Dummy) DisplayMessage(message display.MessageInfo) { 21 | d.logger.WithFields(log.Fields{"message": message}).Info("Displaying message") 22 | } 23 | 24 | func (d *Dummy) Clear() { 25 | d.logger.Info("Clearing display") 26 | } 27 | 28 | func (d *Dummy) Cleanup() { 29 | d.logger.Info("Cleaning up display") 30 | } 31 | 32 | func (d *Dummy) GetType() string { 33 | return TypeDummy 34 | } 35 | -------------------------------------------------------------------------------- /pkg/display/i18n/i18n_test.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "github.com/stretchr/testify/suite" 5 | "testing" 6 | ) 7 | 8 | type I18NTestSuite struct { 9 | suite.Suite 10 | } 11 | 12 | func (suite *I18NTestSuite) SetupTest() { 13 | } 14 | 15 | func (suite *I18NTestSuite) Test() { 16 | } 17 | 18 | func TestI18N(t *testing.T) { 19 | suite.Run(t, new(I18NTestSuite)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/display/i18n/messages.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | func TranslateConnectorAvailableMessage(lang string, connectorId int) ([]string, error) { 4 | data := make(map[string]interface{}) 5 | data["Id"] = connectorId 6 | 7 | firstPart, err := Localize(lang, "ConnectorTemplate", data, nil) 8 | secondPart, err := Localize(lang, "ConnectorAvailable", nil, nil) 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | return []string{firstPart, secondPart}, nil 14 | } 15 | 16 | func TranslateConnectorFinishingMessage(lang string, connectorId int) ([]string, error) { 17 | data := make(map[string]interface{}) 18 | data["Id"] = connectorId 19 | 20 | firstPart, err := Localize(lang, "ConnectorFinishing", nil, nil) 21 | secondPart, err := Localize(lang, "ConnectorStopTemplate", data, nil) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return []string{firstPart, secondPart}, nil 27 | } 28 | 29 | func TranslateConnectorFaultedMessage(lang string, connectorId int) ([]string, error) { 30 | data := make(map[string]interface{}) 31 | data["Id"] = connectorId 32 | 33 | firstPart, err := Localize(lang, "ConnectorTemplate", data, nil) 34 | secondPart, err := Localize(lang, "ConnectorFaulted", nil, nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return []string{firstPart, secondPart}, nil 40 | } 41 | 42 | func TranslateConnectorChargingMessage(lang string, connectorId int) ([]string, error) { 43 | data := make(map[string]interface{}) 44 | data["Id"] = connectorId 45 | 46 | firstPart, err := Localize(lang, "ConnectorCharging", nil, nil) 47 | secondPart, err := Localize(lang, "ConnectorStopTemplate", data, nil) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return []string{firstPart, secondPart}, nil 53 | } 54 | 55 | func TranslateWelcomeMessage(lang string) ([]string, error) { 56 | firstPart, err := Localize(lang, "WelcomeMessage", nil, nil) 57 | secondPart, err := Localize(lang, "WelcomeMessage2", nil, nil) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return []string{firstPart, secondPart}, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/display/i18n/translations/active.en.yaml: -------------------------------------------------------------------------------- 1 | ConnectorAvailable: available. 2 | ConnectorCharging: Started charging 3 | ConnectorFaulted: has faulted. 4 | ConnectorFinishing: Stopped charging 5 | ConnectorStopTemplate: at {{.Id}}. 6 | ConnectorTemplate: Connector {{.Id}} 7 | WelcomeMessage: Welcome to 8 | WelcomeMessage2: ChargePi! 9 | -------------------------------------------------------------------------------- /pkg/display/i18n/translations/active.sl.yaml: -------------------------------------------------------------------------------- 1 | ConnectorAvailable: 2 | hash: sha1-9636bdbd71b2eb12321b01059eaff0c2fc811c75 3 | other: je na voljo. 4 | ConnectorCharging: 5 | hash: sha1-c6da238960ec122c63062c4944c70c37c6e2dada 6 | other: Start polnjenja 7 | ConnectorFaulted: 8 | hash: sha1-7779f053982a2915e34e35cb901a227d63988bec 9 | other: je okvarjena. 10 | ConnectorFinishing: 11 | hash: sha1-893a797163164d35a2cf8f8431bf07bd515d3f34 12 | other: Konec polnjenja 13 | ConnectorStopTemplate: 14 | hash: sha1-e740c244c76a2ad6cc892a8782ccb9ec63f2730e 15 | other: na {{.Id}}. 16 | ConnectorTemplate: 17 | hash: sha1-faab2db8985000bfc70c3624e6a430ef9ea8ffea 18 | other: Vticnica {{.Id}} 19 | WelcomeMessage: 20 | hash: sha1-73ecd675a73b631c77eee228ec76d3aef19d84bc 21 | other: Dobrodosli pri 22 | WelcomeMessage2: 23 | hash: sha1-40d75a64dbdabdc3e0e32bbd098e92a5a66e4a2d 24 | other: ChargePi! 25 | -------------------------------------------------------------------------------- /pkg/evcc/dummy.go: -------------------------------------------------------------------------------- 1 | package evcc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Dummy struct { 11 | logger *log.Logger 12 | settings *settings.EvccDummy 13 | notifications chan StateNotification 14 | currentState CarState 15 | maxCurrent float64 16 | } 17 | 18 | func NewDummy(settings *settings.EvccDummy) (*Dummy, error) { 19 | return &Dummy{ 20 | settings: settings, 21 | logger: log.StandardLogger(), 22 | notifications: make(chan StateNotification), 23 | }, nil 24 | } 25 | 26 | func (d *Dummy) Init(ctx context.Context) error { 27 | d.logger.Info("Initializing dummy EVCC") 28 | return nil 29 | } 30 | 31 | func (d *Dummy) EnableCharging() error { 32 | d.logger.Info("Enabling charging") 33 | return nil 34 | } 35 | 36 | func (d *Dummy) DisableCharging() { 37 | d.logger.Info("Disabling charging") 38 | } 39 | 40 | func (d *Dummy) SetMaxChargingCurrent(value float64) error { 41 | d.logger.Infof("Setting max charging current to %f", value) 42 | d.maxCurrent = value 43 | return nil 44 | } 45 | 46 | func (d *Dummy) GetMaxChargingCurrent() float64 { 47 | d.logger.Info("Getting max charging current") 48 | return d.maxCurrent 49 | } 50 | 51 | func (d *Dummy) Lock() { 52 | d.logger.Info("Locking dummy connector") 53 | } 54 | 55 | func (d *Dummy) Unlock() { 56 | d.logger.Info("Unlocking dummy connector") 57 | } 58 | 59 | func (d *Dummy) GetState() CarState { 60 | d.logger.Info("Getting car state") 61 | return d.currentState 62 | } 63 | 64 | func (d *Dummy) GetError() string { 65 | d.logger.Info("Getting dummy error") 66 | return "" 67 | } 68 | 69 | func (d *Dummy) Cleanup() error { 70 | d.logger.Info("Cleaning up dummy") 71 | return nil 72 | } 73 | 74 | func (d *Dummy) GetType() string { 75 | return TypeDummy 76 | } 77 | 78 | func (d *Dummy) GetStatusChangeChannel() <-chan StateNotification { 79 | return d.notifications 80 | } 81 | 82 | func (d *Dummy) SetNotificationChannel(notifications chan StateNotification) { 83 | d.notifications = notifications 84 | } 85 | 86 | func (d *Dummy) Reset() { 87 | d.logger.Info("Resetting dummy") 88 | } 89 | -------------------------------------------------------------------------------- /pkg/evcc/evcc.go: -------------------------------------------------------------------------------- 1 | package evcc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 7 | ) 8 | 9 | const ( 10 | PhoenixEMCPPPETH = "EM-CP-PP-ETH" 11 | Relay = "Relay" 12 | Western = "Western" 13 | TypeDummy = "Dummy" 14 | ) 15 | 16 | type EVCC interface { 17 | Init(ctx context.Context) error 18 | EnableCharging() error 19 | DisableCharging() 20 | SetMaxChargingCurrent(value float64) error 21 | GetMaxChargingCurrent() float64 22 | Lock() 23 | Unlock() 24 | GetState() CarState 25 | GetError() string 26 | Cleanup() error 27 | GetType() string 28 | GetStatusChangeChannel() <-chan StateNotification 29 | SetNotificationChannel(notifications chan StateNotification) 30 | Reset() 31 | // SelfCheck() error 32 | } 33 | 34 | // NewEVCCFromType creates a new EVCC instance based on the provided type. 35 | func NewEVCCFromType(evccSettings settings.EVCC) (EVCC, error) { 36 | switch evccSettings.Type { 37 | case Relay: 38 | return NewRelay(evccSettings.Relay) 39 | case TypeDummy: 40 | return NewDummy(evccSettings.Dummy) 41 | default: 42 | return nil, nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/evcc/states.go: -------------------------------------------------------------------------------- 1 | package evcc 2 | 3 | const ( 4 | // Car is unplugged 5 | StateA1 = CarState("A1") 6 | StateA2 = CarState("A2") 7 | 8 | // Car connected 9 | StateB1 = CarState("B1") 10 | StateB2 = CarState("B2") 11 | 12 | // Car requests to charge 13 | StateC1 = CarState("C1") 14 | // EVCC allowed charging 15 | StateC2 = CarState("C2") 16 | 17 | // Charging with ventilation 18 | StateD1 = CarState("D1") 19 | StateD2 = CarState("D2") 20 | 21 | // Error state 22 | StateE = CarState("E") 23 | 24 | // Fault state 25 | StateF = CarState("F") 26 | ) 27 | 28 | type ( 29 | CarState string 30 | 31 | StateNotification struct { 32 | State CarState 33 | Error string 34 | } 35 | ) 36 | 37 | func NewStateNotification(state CarState, error string) StateNotification { 38 | return StateNotification{State: state, Error: error} 39 | } 40 | 41 | func IsStateValid(state CarState) bool { 42 | switch state { 43 | case StateA1, 44 | StateA2, 45 | StateB1, 46 | StateB2, 47 | StateC1, 48 | StateC2, 49 | StateD1, 50 | StateD2, 51 | StateE, 52 | StateF: 53 | return true 54 | default: 55 | return false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/indicator/dummy.go: -------------------------------------------------------------------------------- 1 | package indicator 2 | 3 | import ( 4 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | type Dummy struct { 9 | logger *log.Logger 10 | settings settings.IndicatorDummy 11 | brightness int 12 | } 13 | 14 | func NewDummy(settings *settings.IndicatorDummy) *Dummy { 15 | return &Dummy{ 16 | settings: *settings, 17 | logger: log.StandardLogger(), 18 | } 19 | } 20 | 21 | func (d *Dummy) ChangeColor(index int, color Color) error { 22 | d.logger.Infof("Changing color of index %d to %s", index, color) 23 | return nil 24 | } 25 | 26 | func (d *Dummy) Blink(index int, times int, color Color) error { 27 | d.logger.Infof("Blinking color of index %d %d times to %s", index, times, color) 28 | return nil 29 | } 30 | 31 | func (d *Dummy) SetBrightness(brightness int) error { 32 | d.logger.Infof("Setting brightness to %d", brightness) 33 | d.brightness = brightness 34 | return nil 35 | } 36 | 37 | func (d *Dummy) GetBrightness() int { 38 | d.logger.Info("Getting brightness") 39 | return d.brightness 40 | } 41 | 42 | func (d *Dummy) Cleanup() { 43 | d.logger.Info("Cleaning up indicator") 44 | } 45 | 46 | func (d *Dummy) GetType() string { 47 | return TypeDummy 48 | } 49 | -------------------------------------------------------------------------------- /pkg/indicator/indicator.go: -------------------------------------------------------------------------------- 1 | package indicator 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ChargePi/ChargePi-go/internal/pkg/util" 7 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // color constants 12 | const ( 13 | Off = Color("Off") 14 | White = Color("White") 15 | Red = Color("Red") 16 | Green = Color("Green") 17 | Blue = Color("Blue") 18 | Yellow = Color("Yellow") 19 | Orange = Color("Orange") 20 | ) 21 | 22 | // Supported types 23 | const ( 24 | TypeWS281x = "WS281x" 25 | TypeDummy = "dummy" 26 | ) 27 | 28 | var ( 29 | ErrInvalidIndex = errors.New("invalid index") 30 | ErrInvalidPin = errors.New("invalid data pin number") 31 | ErrInvalidNumberOfLeds = errors.New("number of leds must be greater than zero") 32 | ) 33 | 34 | type ( 35 | Color string 36 | 37 | // Indicator is an abstraction layer for connector status indication, usually an RGB LED strip. 38 | Indicator interface { 39 | ChangeColor(index int, color Color) error 40 | Blink(index int, times int, color Color) error 41 | SetBrightness(brightness int) error 42 | GetBrightness() int 43 | Cleanup() 44 | GetType() string 45 | } 46 | ) 47 | 48 | // NewIndicator constructs the Indicator based on the type provided by the settings file. 49 | func NewIndicator(stripLength int, indicator settings.Indicator) Indicator { 50 | if indicator.Enabled { 51 | 52 | // Last LED is used to indicate card read 53 | if indicator.IndicateCardRead { 54 | stripLength++ 55 | } 56 | 57 | log.Infof("Preparing Indicator from config: %s", indicator.Type) 58 | switch indicator.Type { 59 | case TypeWS281x: 60 | if util.IsNilInterfaceOrPointer(indicator.WS281x) { 61 | return nil 62 | } 63 | 64 | ledStrip, ledError := NewWS281xStrip(stripLength, indicator.WS281x.DataPin) 65 | if ledError != nil { 66 | log.WithError(ledError).Errorf("Error creating indicator") 67 | return nil 68 | } 69 | 70 | return ledStrip 71 | case TypeDummy: 72 | return NewDummy(indicator.IndicatorDummy) 73 | default: 74 | return nil 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/models/ocpp/data-transfer-charge-point.go: -------------------------------------------------------------------------------- 1 | package ocpp 2 | 3 | type DataTransferChargePointInfo struct { 4 | // AC or DC 5 | Type string `json:"type" yaml:"type" mapstructure:"type"` 6 | // in kW 7 | MaxPower float32 `json:"maxPower" yaml:"maxPower" mapstructure:"maxPower"` 8 | } 9 | 10 | func NewChargePointInfo(chargePointType string, maxPower float32) DataTransferChargePointInfo { 11 | return DataTransferChargePointInfo{ 12 | Type: chargePointType, 13 | MaxPower: maxPower, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/models/ocpp/data-transfer-connector.go: -------------------------------------------------------------------------------- 1 | package ocpp 2 | 3 | type ( 4 | DataTransferEVSEInfo struct { 5 | EvseId int `json:"evseId" yaml:"evseId" mapstructure:"evseId"` 6 | MaxPower float32 `json:"maxPower,omitempty" yaml:"maxPower" mapstructure:"maxPower"` 7 | Connectors []Connector `json:"connectors,omitempty" yaml:"connectors" mapstructure:"connectors"` 8 | } 9 | 10 | Connector struct { 11 | ConnectorId int `json:"connectorId,omitempty" yaml:"connectorId" mapstructure:"connectorId"` 12 | Type string `json:"type,omitempty" yaml:"type" mapstructure:"type"` 13 | } 14 | ) 15 | 16 | func NewEvseInfo(evseId int, maxPower float32, connectors ...Connector) DataTransferEVSEInfo { 17 | return DataTransferEVSEInfo{ 18 | EvseId: evseId, 19 | MaxPower: maxPower, 20 | Connectors: connectors, 21 | } 22 | } 23 | 24 | func NewConnector(connectorId int, connectorType string) Connector { 25 | return Connector{ 26 | ConnectorId: connectorId, 27 | Type: connectorType, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/models/ocpp/version.go: -------------------------------------------------------------------------------- 1 | package ocpp 2 | 3 | const ( 4 | OCPP16 = ProtocolVersion("1.6") 5 | OCPP201 = ProtocolVersion("2.0.1") 6 | ) 7 | 8 | type ProtocolVersion string 9 | -------------------------------------------------------------------------------- /pkg/models/settings/display.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | Display struct { 5 | // Enable or disable the display from the configuration 6 | IsEnabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty" mapstructure:"enabled,omitempty"` 7 | 8 | // Enable the display to be controller remotely through OCPP 9 | RemoteEnabled bool `json:"remote,omitempty" yaml:"remote,omitempty" mapstructure:"remote,omitempty"` 10 | 11 | // Display driver/type of the display - can be a direct implementation of the driver or an HTTP server 12 | Driver string `json:"driver,omitempty" yaml:"driver,omitempty" mapstructure:"driver,omitempty"` 13 | 14 | // Default display language 15 | Language string `json:"language,omitempty" yaml:"language,omitempty" mapstructure:"language,omitempty"` 16 | 17 | // Hitachi HD44780 display configuration details 18 | HD44780 *HD44780 `json:"hd44780,omitempty" yaml:"hd44780,omitempty" mapstructure:"hd44780,omitempty"` 19 | 20 | // Dummy display configuration details 21 | DisplayDummy *DisplayDummy `json:"dummy,omitempty" yaml:"dummy,omitempty" mapstructure:"dummy,omitempty"` 22 | } 23 | 24 | HD44780 struct { 25 | I2C I2C `fig:"i2c" json:"i2c,omitempty" yaml:"i2c,omitempty" mapstructure:"i2c,omitempty"` 26 | } 27 | 28 | DisplayDummy struct { 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /pkg/models/settings/evcc.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type EVCC struct { 4 | Type string `validate:"required" json:"type,omitempty" yaml:"type" mapstructure:"type"` 5 | // Based on the type, get the connection details 6 | Relay *Relay `json:"relay,omitempty" yaml:"relay,omitempty" mapstructure:"relay,omitempty"` 7 | 8 | Serial *Serial `json:"serial,omitempty" yaml:"serial,omitempty" mapstructure:"serial,omitempty"` 9 | 10 | Modbus *ModBus `json:"modbus,omitempty" yaml:"modbus,omitempty" mapstructure:"modbus,omitempty"` 11 | 12 | Dummy *EvccDummy `json:"dummy,omitempty" yaml:"dummy,omitempty" mapstructure:"dummy,omitempty"` 13 | } 14 | 15 | type Relay struct { 16 | RelayPin int `json:"relayPin,omitempty" yaml:"relayPin,omitempty" mapstructure:"relayPin,omitempty"` 17 | InverseLogic bool `json:"inverseLogic,omitempty" yaml:"inverseLogic,omitempty" mapstructure:"inverseLogic,omitempty"` 18 | } 19 | 20 | type EvccDummy struct { 21 | } 22 | -------------------------------------------------------------------------------- /pkg/models/settings/hardware-interfaces.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | I2C struct { 5 | Address string `json:"address,omitempty" yaml:"address,omitempty" mapstructure:"address,omitempty"` 6 | Bus int `json:"bus,omitempty" yaml:"bus,omitempty" mapstructure:"bus,omitempty"` 7 | } 8 | 9 | SPI struct { 10 | ChipSelect int `json:"chipSelect,omitempty" yaml:"chipSelect,omitempty" mapstructure:"chipSelect,omitempty"` 11 | Bus int `json:"bus,omitempty" yaml:"bus,omitempty" mapstructure:"bus,omitempty"` 12 | } 13 | 14 | ModBus struct { 15 | DeviceAddress string `json:"deviceAddress,omitempty" yaml:"deviceAddress,omitempty" mapstructure:"deviceAddress,omitempty"` 16 | Protocol string `json:"protocol,omitempty" yaml:"protocol,omitempty" mapstructure:"protocol,omitempty"` 17 | } 18 | 19 | Serial struct { 20 | DeviceAddress string `json:"deviceAddress,omitempty" yaml:"deviceAddress,omitempty" mapstructure:"deviceAddress,omitempty"` 21 | BaudRate uint `json:"baudRate,omitempty" yaml:"baudRate,omitempty" mapstructure:"baudRate,omitempty"` 22 | Parity uint `json:"parity,omitempty" yaml:"parity,omitempty" mapstructure:"parity,omitempty"` 23 | DataBits uint8 `json:"dataBits,omitempty" yaml:"dataBits,omitempty" mapstructure:"dataBits,omitempty"` 24 | StopBits uint8 `json:"stopBits,omitempty" yaml:"stopBits,omitempty" mapstructure:"stopBits,omitempty"` 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /pkg/models/settings/indicator.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | Indicator struct { 5 | 6 | // Enable or disable the indicator 7 | Enabled bool `json:"enabled" yaml:"enabled" mapstructure:"enabled"` 8 | 9 | // Indicate card read 10 | IndicateCardRead bool `json:"indicateCardRead,omitempty" yaml:"indicateCardRead,omitempty" mapstructure:"indicateCardRead,omitempty"` 11 | 12 | // Type of indicator 13 | Type string `json:"type,omitempty" yaml:"type" mapstructure:"type"` 14 | 15 | // Statuses 16 | IndicatorMappings *IndicatorStatusMapping `json:"statuses,omitempty" yaml:"statuses,omitempty" mapstructure:"statuses,omitempty"` 17 | 18 | // Based on the type, get the connection details 19 | WS281x *WS281x `json:"ws281x,omitempty" yaml:"ws281x,omitempty" mapstructure:"ws281x,omitempty"` 20 | 21 | // Dummy indicator 22 | IndicatorDummy *IndicatorDummy `json:"dummy,omitempty" yaml:"dummy,omitempty" mapstructure:"dummy,omitempty"` 23 | } 24 | 25 | WS281x struct { 26 | DataPin int `json:"dataPin,omitempty" yaml:"dataPin,omitempty" mapstructure:"dataPin,omitempty"` 27 | Invert bool `json:"invert,omitempty" yaml:"invert,omitempty" mapstructure:"invert,omitempty"` 28 | } 29 | 30 | IndicatorDummy struct { 31 | } 32 | 33 | IndicatorStatusMapping struct { 34 | Available string `json:"available,omitempty" yaml:"available,omitempty" mapstructure:"available,omitempty" validate:"hexcolor"` 35 | Reserved string `json:"reserved,omitempty" yaml:"reserved,omitempty" mapstructure:"reserved,omitempty" validate:"hexcolor"` 36 | Preparing string `json:"preparing,omitempty" yaml:"preparing,omitempty" mapstructure:"preparing,omitempty" validate:"hexcolor"` 37 | Charging string `json:"charging,omitempty" yaml:"charging,omitempty" mapstructure:"charging,omitempty" validate:"hexcolor"` 38 | Finishing string `json:"finishing,omitempty" yaml:"finishing,omitempty" mapstructure:"finishing,omitempty" validate:"hexcolor"` 39 | Fault string `json:"fault,omitempty" yaml:"fault,omitempty" mapstructure:"fault,omitempty" validate:"hexcolor"` 40 | Error string `json:"error,omitempty" yaml:"error,omitempty" mapstructure:"error,omitempty" validate:"hexcolor"` 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/models/settings/power-meter.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | PowerMeter struct { 5 | Enabled bool `json:"enabled" yaml:"enabled" mapstructure:"enabled"` 6 | 7 | Type string `json:"type,omitempty" yaml:"type,omitempty" mapstructure:"type,omitempty"` 8 | 9 | // Based on the type, get the connection details 10 | // For smarter energy meters, using Modbus RTU or similar based on the device type 11 | ModBus *ModBus `json:"modbus,omitempty" yaml:"modbus,omitempty" mapstructure:"modbus,omitempty"` 12 | 13 | SPI *SPI `json:"spi,omitempty" yaml:"spi,omitempty" mapstructure:"spi,omitempty"` 14 | 15 | // CS5460 specific details 16 | CS5460 *CS5460 `json:"cs5460,omitempty" yaml:"cs5460,omitempty" mapstructure:"cs5460,omitempty"` 17 | 18 | // Dummy power meter for testing 19 | PowerMeterDummy *PowerMeterDummy `json:"dummy,omitempty" yaml:"dummy,omitempty" mapstructure:"dummy,omitempty"` 20 | } 21 | 22 | CS5460 struct { 23 | ShuntOffset float64 `json:"shuntOffset,omitempty" yaml:"shuntOffset,omitempty" mapstructure:"shuntOffset,omitempty"` 24 | VoltageDividerOffset float64 `json:"voltageDividerOffset,omitempty" yaml:"voltageDividerOffset,omitempty" mapstructure:"voltageDividerOffset,omitempty"` 25 | } 26 | 27 | PowerMeterDummy struct { 28 | Voltage float64 `json:"voltage,omitempty" yaml:"voltage,omitempty" mapstructure:"voltage,omitempty"` 29 | VoltageBehaviour string `json:"voltageBehaviour,omitempty" yaml:"voltageBehaviour,omitempty" mapstructure:"voltageBehaviour,omitempty"` 30 | BaseCurrent float64 `json:"baseCurrent,omitempty" yaml:"baseCurrent,omitempty" mapstructure:"baseCurrent,omitempty"` 31 | CurrentBehaviour string `json:"currentBehaviour,omitempty" yaml:"currentBehaviour,omitempty" mapstructure:"currentBehaviour,omitempty"` 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /pkg/models/settings/reader.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type ( 4 | TagReader struct { 5 | IsEnabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty" mapstructure:"enabled,omitempty"` 6 | ReaderModel string `json:"model,omitempty" yaml:"model,omitempty" mapstructure:"model,omitempty"` 7 | PN532 *PN532 `json:"pn532,omitempty" yaml:"pn532,omitempty" mapstructure:"pn532,omitempty"` 8 | DummyReader *DummyReader `json:"dummy,omitempty" yaml:"dummy,omitempty" mapstructure:"dummy,omitempty"` 9 | } 10 | 11 | PN532 struct { 12 | Device string `json:"deviceAddress,omitempty" yaml:"deviceAddress,omitempty" mapstructure:"deviceAddress,omitempty"` 13 | ResetPin int `json:"resetPin,omitempty" yaml:"resetPin,omitempty" mapstructure:"resetPin,omitempty"` 14 | } 15 | 16 | DummyReader struct { 17 | TagIds []string `json:"tagIds,omitempty" yaml:"tagIds,omitempty" mapstructure:"tagIds,omitempty"` 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/observability/logging/formats.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | type ( 4 | LogType string 5 | LogFormat string 6 | ) 7 | 8 | const ( 9 | RemoteLogging = LogType("remote") 10 | ConsoleLogging = LogType("console") 11 | 12 | Syslog = LogFormat("syslog") 13 | Gelf = LogFormat("gelf") 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/observability/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "log/syslog" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/models/settings" 8 | "github.com/ChargePi/ChargePi-go/internal/pkg/util" 9 | graylog "github.com/gemnasium/logrus-graylog-hook/v3" 10 | "github.com/lorenzodonini/ocpp-go/ocppj" 11 | "github.com/lorenzodonini/ocpp-go/ws" 12 | "github.com/orandin/lumberjackrus" 13 | log "github.com/sirupsen/logrus" 14 | lSyslog "github.com/sirupsen/logrus/hooks/syslog" 15 | ) 16 | 17 | const LogFileName = "chargepi.log" 18 | const LogFileDir = "/var/log/chargepi" 19 | 20 | // Setup setup logs 21 | func Setup(logger *log.Logger, loggingConfig settings.Logging, isDebug bool) { 22 | // Default logging settings 23 | logLevel := log.InfoLevel 24 | formatter := &log.JSONFormatter{} 25 | logger.SetFormatter(formatter) 26 | 27 | if isDebug { 28 | // Set underlying library loggers to debug level 29 | logLevel = log.DebugLevel 30 | ocppj.SetLogger(logger) 31 | ws.SetLogger(logger) 32 | } 33 | 34 | logger.SetLevel(logLevel) 35 | 36 | // Setup file logging 37 | fileLogging(logger, fmt.Sprintf("%s/%s", LogFileDir, LogFileName)) 38 | 39 | // Setup remote logging, if configured 40 | for _, logType := range loggingConfig.LogTypes { 41 | switch LogType(logType.Type) { 42 | case RemoteLogging: 43 | if util.IsNilInterfaceOrPointer(logType.Address) && util.IsNilInterfaceOrPointer(logType.Format) { 44 | remoteLogging(logger, *logType.Address, LogFormat(*logType.Format)) 45 | } 46 | case ConsoleLogging: 47 | } 48 | } 49 | } 50 | 51 | func fileLogging(logger *log.Logger, fileName string) { 52 | hook, err := lumberjackrus.NewHook( 53 | &lumberjackrus.LogFile{ 54 | Filename: fileName, 55 | MaxSize: 200, 56 | MaxBackups: 20, 57 | MaxAge: 1, 58 | Compress: false, 59 | LocalTime: false, 60 | }, 61 | logger.GetLevel(), 62 | logger.Formatter, 63 | nil, 64 | ) 65 | 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | logger.AddHook(hook) 71 | } 72 | 73 | // remoteLogging sends logs remotely to Graylog or any Syslog receiver. 74 | func remoteLogging(logger *log.Logger, address string, format LogFormat) { 75 | var ( 76 | hook log.Hook 77 | err error 78 | ) 79 | 80 | switch format { 81 | case Gelf: 82 | graylogHook := graylog.NewAsyncGraylogHook(address, map[string]interface{}{}) 83 | hook = graylogHook 84 | case Syslog: 85 | hook, err = lSyslog.NewSyslogHook( 86 | "tcp", 87 | address, 88 | syslog.LOG_WARNING, 89 | "chargePi", 90 | ) 91 | default: 92 | return 93 | } 94 | 95 | if err == nil { 96 | logger.AddHook(hook) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/power-meter/dummy.go: -------------------------------------------------------------------------------- 1 | package powerMeter 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 9 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type Dummy struct { 14 | logger log.FieldLogger 15 | settings settings.PowerMeterDummy 16 | energy atomic.Int64 17 | lastSample *time.Time 18 | } 19 | 20 | func NewDummy(settings *settings.PowerMeterDummy) (*Dummy, error) { 21 | if settings == nil { 22 | return nil, ErrInvalidConnectionSettings 23 | } 24 | 25 | return &Dummy{ 26 | settings: *settings, 27 | logger: log.StandardLogger().WithField("component", "power-meter-dummy"), 28 | energy: atomic.Int64{}, 29 | }, nil 30 | } 31 | 32 | func (d *Dummy) Init(ctx context.Context) error { 33 | d.logger.Info("Initializing power meter") 34 | return nil 35 | } 36 | 37 | func (d *Dummy) Reset() { 38 | d.logger.Info("Resetting power meter") 39 | } 40 | 41 | func (d *Dummy) GetEnergy() (*types.SampledValue, error) { 42 | d.logger.Info("Getting energy") 43 | return nil, nil 44 | } 45 | 46 | func (d *Dummy) GetPower(phase int) (*types.SampledValue, error) { 47 | d.logger.WithField("phase", phase).Info("Getting power") 48 | 49 | return nil, nil 50 | } 51 | 52 | func (d *Dummy) GetReactivePower(phase int) (*types.SampledValue, error) { 53 | d.logger.WithField("phase", phase).Info("Getting reactive power") 54 | return nil, nil 55 | } 56 | 57 | func (d *Dummy) GetApparentPower(phase int) (*types.SampledValue, error) { 58 | d.logger.WithField("phase", phase).Info("Getting apparent power") 59 | return nil, nil 60 | } 61 | 62 | func (d *Dummy) GetCurrent(phase int) (*types.SampledValue, error) { 63 | d.logger.WithField("phase", phase).Info("Getting current") 64 | 65 | return nil, nil 66 | } 67 | 68 | func (d *Dummy) GetVoltage(phase int) (*types.SampledValue, error) { 69 | d.logger.WithField("phase", phase).Info("Getting voltage") 70 | return nil, nil 71 | } 72 | 73 | func (d *Dummy) GetType() string { 74 | return TypeDummy 75 | } 76 | 77 | func (d *Dummy) Cleanup() { 78 | d.logger.Info("Cleaning up power meter") 79 | } 80 | -------------------------------------------------------------------------------- /pkg/power-meter/power-meter.go: -------------------------------------------------------------------------------- 1 | package powerMeter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/ChargePi/ChargePi-go/internal/pkg/util" 8 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 9 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Supported power meters 14 | const ( 15 | TypeC5460A = "cs5460a" 16 | TypeDummy = "dummy" 17 | ) 18 | 19 | var ( 20 | ErrPowerMeterUnsupported = errors.New("power meter type not supported") 21 | ErrPowerMeterDisabled = errors.New("power meter not enabled") 22 | ErrInvalidConnectionSettings = errors.New("invalid power meter connection settings") 23 | ) 24 | 25 | // PowerMeter is an abstraction for measurement hardware. 26 | type PowerMeter interface { 27 | Init(ctx context.Context) error 28 | Cleanup() 29 | Reset() 30 | GetEnergy() (*types.SampledValue, error) 31 | GetPower(phase int) (*types.SampledValue, error) 32 | GetReactivePower(phase int) (*types.SampledValue, error) 33 | GetApparentPower(phase int) (*types.SampledValue, error) 34 | GetCurrent(phase int) (*types.SampledValue, error) 35 | GetVoltage(phase int) (*types.SampledValue, error) 36 | GetType() string 37 | } 38 | 39 | // NewPowerMeter creates a new power meter based on the connector settings. 40 | func NewPowerMeter(meterSettings settings.PowerMeter) (PowerMeter, error) { 41 | if meterSettings.Enabled { 42 | log.Infof("Creating a new power meter: %s", meterSettings.Type) 43 | 44 | switch meterSettings.Type { 45 | case TypeC5460A: 46 | if util.IsNilInterfaceOrPointer(meterSettings.SPI) { 47 | return nil, ErrInvalidConnectionSettings 48 | } 49 | 50 | powerMeter, err := NewCS5460PowerMeter( 51 | meterSettings.SPI.ChipSelect, 52 | meterSettings.SPI.Bus, 53 | meterSettings.CS5460.ShuntOffset, 54 | meterSettings.CS5460.VoltageDividerOffset, 55 | ) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return powerMeter, nil 61 | case TypeDummy: 62 | return NewDummy(meterSettings.PowerMeterDummy) 63 | default: 64 | return nil, ErrPowerMeterUnsupported 65 | } 66 | } 67 | 68 | return nil, ErrPowerMeterDisabled 69 | } 70 | -------------------------------------------------------------------------------- /pkg/reader/TagReader.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package reader 5 | 6 | import ( 7 | "context" 8 | ) 9 | 10 | type TagReader struct { 11 | TagChannel chan string 12 | DeviceAddress string 13 | ResetPin int 14 | } 15 | 16 | // init Initialize the NFC/RFID tag reader. Establish the connection and set up the reader. 17 | func (reader *TagReader) init() { 18 | } 19 | 20 | // ListenForTags poll the reader for NFC/RFID tags. Uses multiple modulations for different standards. 21 | // Send the ID of the detected card through the TagChannel. If there is a problem with the reader, 22 | // hardware Reset the device. 23 | func (reader *TagReader) ListenForTags(ctx context.Context) { 24 | } 25 | 26 | func (reader *TagReader) GetTagChannel() <-chan string { 27 | return reader.TagChannel 28 | } 29 | 30 | // Cleanup Close the reader device connection. 31 | func (reader *TagReader) Cleanup() { 32 | close(reader.TagChannel) 33 | } 34 | 35 | // Reset Implements the hardware reset by pulling the resetPin low and then releasing. 36 | func (reader *TagReader) Reset() { 37 | } 38 | 39 | func (reader *TagReader) GetType() string { 40 | return "" 41 | } 42 | -------------------------------------------------------------------------------- /pkg/reader/dummy.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Dummy struct { 11 | logger *log.Logger 12 | listener chan string 13 | settings settings.DummyReader 14 | tagIndex int 15 | } 16 | 17 | func NewDummy(settings *settings.DummyReader) (*Dummy, error) { 18 | return &Dummy{ 19 | settings: *settings, 20 | listener: make(chan string), 21 | tagIndex: 0, 22 | }, nil 23 | } 24 | 25 | func (d *Dummy) ListenForTags(ctx context.Context) { 26 | d.logger.Info("Listening for tags") 27 | 28 | tag := d.settings.TagIds[d.tagIndex] 29 | 30 | d.listener <- tag 31 | d.logger.Infof("Tag %s read", tag) 32 | d.tagIndex++ 33 | } 34 | 35 | func (d *Dummy) Cleanup() { 36 | d.logger.Info("Cleaning up reader") 37 | } 38 | 39 | func (d *Dummy) Reset() { 40 | d.logger.Info("Resetting reader") 41 | } 42 | 43 | func (d *Dummy) GetTagChannel() <-chan string { 44 | return d.listener 45 | } 46 | 47 | func (d *Dummy) GetType() string { 48 | return TypeDummy 49 | } 50 | -------------------------------------------------------------------------------- /pkg/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/ChargePi/ChargePi-go/pkg/models/settings" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Supported readers - by libnfc 12 | const ( 13 | PN532 = "PN532" 14 | ACR122 = "ACR122" 15 | PN533 = "PN533" 16 | BR500 = "BR500" 17 | R502 = "R502" 18 | TypeDummy = "dummy" 19 | ) 20 | 21 | var ( 22 | ErrReaderUnsupported = errors.New("reader type unsupported") 23 | ErrReaderDisabled = errors.New("reader disabled") 24 | ) 25 | 26 | // Reader is an abstraction for an RFID/NFC tag reader. 27 | type Reader interface { 28 | ListenForTags(ctx context.Context) 29 | Cleanup() 30 | Reset() 31 | GetTagChannel() <-chan string 32 | GetType() string 33 | } 34 | 35 | // NewTagReader creates an instance of the Reader interface based on the provided configuration. 36 | func NewTagReader(reader settings.TagReader) (Reader, error) { 37 | if reader.IsEnabled { 38 | log.Infof("Preparing tag reader from config: %s", reader.ReaderModel) 39 | switch reader.ReaderModel { 40 | case PN532, ACR122, PN533, BR500, R502: 41 | return NewReader(reader.PN532.Device, reader.ReaderModel, reader.PN532.ResetPin) 42 | case TypeDummy: 43 | return NewDummy(reader.DummyReader) 44 | default: 45 | return nil, ErrReaderUnsupported 46 | } 47 | } 48 | 49 | return nil, ErrReaderDisabled 50 | } 51 | -------------------------------------------------------------------------------- /pkg/util/random-tag-id-generator.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func GenerateRandomTag() string { 10 | tagId := uuid.New().String() 11 | tagId = strings.ReplaceAll(tagId, "-", "") 12 | return tagId[:20] 13 | } 14 | -------------------------------------------------------------------------------- /proto/charge_point/v1/charge_point.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package charge_point.v1; 4 | 5 | import "common/v1/charge_point.proto"; 6 | import "google/protobuf/empty.proto"; 7 | 8 | service ChargePointService { 9 | rpc GetChargePointDetails(google.protobuf.Empty) returns (GetChargePointDetailsResponse) {} 10 | rpc ChangeChargePointDetails(ChangeChargePointDetailsRequest) returns (ChangeChargePointDetailsResponse) {} 11 | rpc GetVersion(google.protobuf.Empty) returns (GetVersionResponse) {} 12 | rpc GetStatus(google.protobuf.Empty) returns (GetStatusResponse) {} 13 | rpc Restart(RestartRequest) returns (google.protobuf.Empty) {} 14 | } 15 | 16 | message ChangeChargePointDetailsRequest { 17 | ChargePointInfo charge_point_info = 1; 18 | } 19 | 20 | message ChangeChargePointDetailsResponse { 21 | string status = 2; 22 | } 23 | 24 | message GetChargePointDetailsResponse { 25 | ChargePointInfo charge_point_info = 1; 26 | } 27 | 28 | message GetVersionResponse { 29 | string version = 1; 30 | } 31 | 32 | message GetStatusResponse { 33 | bool connected = 1; 34 | string status = 2; 35 | } 36 | 37 | message RestartRequest { 38 | string type = 1; 39 | } 40 | 41 | message ConnectionSettings { 42 | optional string charge_point_id = 1; 43 | optional string url = 2; 44 | common.v1.ProtocolVersion protocol_version = 3; 45 | optional string basic_auth_username = 4; 46 | optional string basic_auth_password = 5; 47 | optional string tls_ca_certificate = 6; 48 | optional string tls_client_certificate = 7; 49 | optional string tls_client_private_key = 8; 50 | } 51 | 52 | message ChargePointInfo { 53 | optional string type = 1; 54 | optional float max_power = 2; 55 | OcppInfo ocpp_info = 4; 56 | FreeCharging free_charging = 5; 57 | } 58 | 59 | message OcppInfo { 60 | string vendor = 1; 61 | string model = 2; 62 | optional string iccid = 3; 63 | optional string imsi = 4; 64 | optional string charge_box_serial_number = 5; 65 | optional string charge_point_serial_number = 6; 66 | } 67 | 68 | message FreeCharging { 69 | bool enabled = 1; 70 | optional int32 max_charging_time = 2; 71 | } 72 | -------------------------------------------------------------------------------- /proto/common/v1/charge_point.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common.v1; 4 | 5 | enum ProtocolVersion { 6 | PROTOCOL_VERSION_UNSPECIFIED = 0; 7 | PROTOCOL_VERSION_OCPP_16 = 1; 8 | PROTOCOL_VERSION_OCPP_201 = 2; 9 | } 10 | -------------------------------------------------------------------------------- /proto/common/v1/connector.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common.v1; 4 | 5 | enum ConnectorStatus { 6 | CONNECTOR_STATUS_UNSPECIFIED = 0; 7 | CONNECTOR_STATUS_AVAILABLE = 1; 8 | CONNECTOR_STATUS_PREPARING = 2; 9 | CONNECTOR_STATUS_CHARGING = 3; 10 | CONNECTOR_STATUS_FINISHING = 4; 11 | CONNECTOR_STATUS_UNAVAILABLE = 5; 12 | CONNECTOR_STATUS_SUSPENDED_EVSE = 6; 13 | CONNECTOR_STATUS_SUSPENDED_EV = 7; 14 | CONNECTOR_STATUS_RESERVED = 8; 15 | CONNECTOR_STATUS_FAULTED = 9; 16 | } 17 | 18 | enum ErrorCode { 19 | ERROR_CODE_UNSPECIFIED = 0; 20 | ERROR_CODE_OTHER = 1; 21 | ERROR_CODE_CONNECTOR_LOCK_FAILURE = 3; 22 | ERROR_CODE_EV_COMMUNICATION_ERROR = 4; 23 | ERROR_CODE_GROUND_FAILURE = 5; 24 | ERROR_CODE_HIGH_TEMPERATURE = 6; 25 | ERROR_CODE_INTERNAL_ERROR = 7; 26 | ERROR_CODE_LOCAL_LIST_CONFLICT = 8; 27 | ERROR_CODE_OVER_CURRENT_FAILURE = 9; 28 | ERROR_CODE_OVER_VOLTAGE = 10; 29 | ERROR_CODE_POWER_METER_FAILURE = 11; 30 | ERROR_CODE_POWER_SWITCH_FAILURE = 12; 31 | ERROR_CODE_READER_FAILURE = 13; 32 | ERROR_CODE_RESET_FAILURE = 14; 33 | ERROR_CODE_UNDER_VOLTAGE = 15; 34 | ERROR_CODE_WEAK_SIGNAL = 16; 35 | } 36 | 37 | enum ConnectorType { 38 | CONNECTOR_TYPE_UNSPECIFIED = 0; 39 | CONNECTOR_TYPE_TYPE1 = 1; 40 | CONNECTOR_TYPE_TYPE2 = 2; 41 | CONNECTOR_TYPE_SCHUKO = 3; 42 | CONNECTOR_TYPE_CHADEMO = 4; 43 | } 44 | -------------------------------------------------------------------------------- /proto/common/v1/hardware.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common.v1; 4 | 5 | message EVCC { 6 | string type = 1; 7 | string status = 2; 8 | string error = 3; 9 | optional Serial serial = 4; 10 | optional Modbus modbus = 5; 11 | } 12 | 13 | message PowerMeter { 14 | string type = 1; 15 | bool enabled = 2; 16 | optional Modbus modbus = 3; 17 | optional Spi spi = 4; 18 | } 19 | 20 | message Display { 21 | string type = 1; 22 | bool enabled = 2; 23 | optional string language = 3; 24 | optional I2c i2c = 4; 25 | } 26 | 27 | message TagReader { 28 | string type = 1; 29 | bool enabled = 2; 30 | optional string device_address = 3; 31 | optional int32 reset_pin = 4; 32 | } 33 | 34 | message Indicator { 35 | string type = 1; 36 | bool enabled = 2; 37 | optional bool indicate_card_read = 3; 38 | optional int32 data_pin = 4; 39 | optional bool invert = 5; 40 | } 41 | 42 | message Spi { 43 | int32 chip_select = 1; 44 | int32 bus = 2; 45 | } 46 | 47 | message Modbus { 48 | string protocol = 1; 49 | string device_address = 2; 50 | } 51 | 52 | message I2c { 53 | string address = 1; 54 | int32 bus = 2; 55 | } 56 | 57 | message Serial { 58 | string device_address = 1; 59 | uint32 baud_rate = 2; 60 | uint32 parity = 3; 61 | uint32 data_bits = 4; 62 | uint32 stop_bits = 5; 63 | } 64 | -------------------------------------------------------------------------------- /proto/common/v1/session.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common.v1; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | message Sample { 8 | Consumption consumption = 1; 9 | string measureand = 2; 10 | optional string phase = 3; 11 | optional string location = 4; 12 | optional string context = 5; 13 | google.protobuf.Timestamp timestamp = 6; 14 | } 15 | 16 | message Consumption { 17 | string unit = 1; 18 | float value = 2; 19 | } 20 | -------------------------------------------------------------------------------- /proto/configuration/v1/display.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package configuration.v1; 4 | 5 | import "common/v1/hardware.proto"; 6 | import "google/protobuf/empty.proto"; 7 | 8 | service DisplayService { 9 | rpc SetDisplaySettings(SetDisplaySettingsRequest) returns (SetDisplaySettingsResponse) {} 10 | rpc GetDisplaySettings(google.protobuf.Empty) returns (GetDisplaySettingsResponse) {} 11 | } 12 | /*------------------ Display ------------------------ */ 13 | 14 | message SetDisplaySettingsRequest { 15 | Display display = 1; 16 | } 17 | 18 | message SetDisplaySettingsResponse { 19 | string status = 1; 20 | } 21 | 22 | message GetDisplaySettingsResponse { 23 | Display display = 1; 24 | } 25 | 26 | message Display { 27 | string type = 1; 28 | bool enabled = 2; 29 | optional string language = 3; 30 | optional common.v1.I2c i2c = 4; 31 | } 32 | -------------------------------------------------------------------------------- /proto/configuration/v1/indicator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package configuration.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service IndicatorService { 8 | // LED indicator 9 | rpc SetIndicatorSettings(SetIndicatorSettingsRequest) returns (SetIndicatorSettingsResponse) {} 10 | rpc GetIndicatorSettings(google.protobuf.Empty) returns (GetIndicatorSettingsResponse) {} 11 | } 12 | 13 | /*------------------ Indication ------------------------ */ 14 | 15 | message SetIndicatorSettingsRequest { 16 | Indicator indicator = 1; 17 | } 18 | 19 | message SetIndicatorSettingsResponse { 20 | string status = 1; 21 | } 22 | 23 | message GetIndicatorSettingsResponse { 24 | Indicator indicator = 1; 25 | } 26 | 27 | message Indicator { 28 | string type = 1; 29 | bool enabled = 2; 30 | optional bool indicate_card_read = 3; 31 | optional int32 data_pin = 4; 32 | optional bool invert = 5; 33 | } 34 | -------------------------------------------------------------------------------- /proto/configuration/v1/ocpp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package configuration.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service ConfigurationService { 8 | rpc GetVariables(google.protobuf.Empty) returns (GetVariablesResponse) {} 9 | rpc SetVariables(SetVariablesRequest) returns (SetVariablesResponse) {} 10 | rpc GetVariable(GetVariableRequest) returns (GetVariableResponse) {} 11 | } 12 | 13 | message GetVariableRequest { 14 | string key = 1; 15 | } 16 | 17 | message GetVariableResponse { 18 | string key = 1; 19 | bool read_only = 2; 20 | optional string value = 3; 21 | } 22 | 23 | message GetVariablesResponse { 24 | repeated OcppVariable variables = 1; 25 | } 26 | 27 | message SetVariablesRequest { 28 | repeated OcppVariable variables = 1; 29 | } 30 | 31 | message SetVariablesResponse { 32 | repeated string statuses = 1; 33 | } 34 | 35 | message OcppVariable { 36 | string key = 1; 37 | bool read_only = 2; 38 | optional string value = 3; 39 | } 40 | -------------------------------------------------------------------------------- /proto/configuration/v1/tag_reader.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package configuration.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service TagReaderService { 8 | rpc SetReaderSettings(SetReaderSettingsRequest) returns (SetReaderSettingsResponse) {} 9 | rpc GetReaderSettings(google.protobuf.Empty) returns (GetReaderSettingsResponse) {} 10 | } 11 | 12 | message SetReaderSettingsRequest { 13 | Reader reader = 1; 14 | } 15 | 16 | message SetReaderSettingsResponse { 17 | string status = 1; 18 | } 19 | 20 | message GetReaderSettingsResponse { 21 | Reader reader = 1; 22 | } 23 | 24 | message Reader { 25 | string type = 1; 26 | bool enabled = 2; 27 | optional string device_address = 3; 28 | optional int32 reset_pin = 4; 29 | } 30 | -------------------------------------------------------------------------------- /proto/connection/v1/connection.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package connection.v1; 4 | 5 | import "common/v1/charge_point.proto"; 6 | import "google/protobuf/empty.proto"; 7 | 8 | service ConnectionService { 9 | // Connection settings 10 | rpc GetConnectionDetails(google.protobuf.Empty) returns (GetConnectionDetailsResponse) {} 11 | rpc ChangeConnectionDetails(ChangeConnectionDetailsRequest) returns (ChangeConnectionDetailsResponse) {} 12 | } 13 | 14 | /*------------------ ConnectionDetails ------------------------ */ 15 | 16 | message GetConnectionDetailsResponse { 17 | ConnectionSettings connection_settings = 1; 18 | } 19 | 20 | message ChangeConnectionDetailsRequest { 21 | ConnectionSettings connection_settings = 1; 22 | } 23 | 24 | message ChangeConnectionDetailsResponse { 25 | ConnectionSettings connection_settings = 1; 26 | string status = 2; 27 | } 28 | 29 | message ConnectionSettings { 30 | optional string charge_point_id = 1; 31 | optional string url = 2; 32 | common.v1.ProtocolVersion protocol_version = 3; 33 | optional string basic_auth_username = 4; 34 | optional string basic_auth_password = 5; 35 | optional string tls_ca_certificate = 6; 36 | optional string tls_client_certificate = 7; 37 | optional string tls_client_private_key = 8; 38 | } 39 | -------------------------------------------------------------------------------- /proto/evse/v1/evcc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package evse.v1; 4 | 5 | import "common/v1/hardware.proto"; 6 | 7 | message EVCC { 8 | string type = 1; 9 | string status = 2; 10 | string error = 3; 11 | optional common.v1.Serial serial = 4; 12 | optional common.v1.Modbus modbus = 5; 13 | } 14 | -------------------------------------------------------------------------------- /proto/evse/v1/evse.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package evse.v1; 4 | 5 | import "common/v1/connector.proto"; 6 | import "common/v1/session.proto"; 7 | import "evse/v1/evcc.proto"; 8 | import "evse/v1/power_meter.proto"; 9 | import "google/protobuf/empty.proto"; 10 | import "google/protobuf/timestamp.proto"; 11 | 12 | service EvseService { 13 | rpc AddEVSE(AddEVSERequest) returns (AddEVSEResponse) {} 14 | rpc GetEVSEs(google.protobuf.Empty) returns (GetEVSEsResponse) {} 15 | rpc GetEVSE(GetEVSERequest) returns (GetEVSEResponse) {} 16 | rpc SetEVCC(SetEVCCRequest) returns (SetEVCCResponse) {} 17 | rpc SetPowerMeter(SetPowerMeterRequest) returns (SetPowerMeterResponse) {} 18 | rpc GetUsageForEVSE(GetUsageForEVSERequest) returns (stream GetUsageForEVSEResponse) {} 19 | } 20 | 21 | message GetEVSEsResponse { 22 | repeated EVSE evses = 1; 23 | } 24 | 25 | message GetEVSERequest { 26 | int32 evse_id = 1; 27 | } 28 | 29 | message GetEVSEResponse { 30 | EVSE evse = 1; 31 | } 32 | 33 | message AddEVSERequest { 34 | int32 evse_id = 1; 35 | EVCC evcc = 2; 36 | optional PowerMeter power_meter = 3; 37 | } 38 | 39 | message AddEVSEResponse { 40 | string status = 1; 41 | EVSE evse = 2; 42 | } 43 | 44 | message SetEVCCRequest { 45 | int32 evse_id = 1; 46 | EVCC evcc = 2; 47 | } 48 | 49 | message SetEVCCResponse { 50 | string type = 1; 51 | } 52 | 53 | message SetPowerMeterRequest { 54 | int32 evse_id = 1; 55 | PowerMeter power_meter = 2; 56 | } 57 | 58 | message SetPowerMeterResponse { 59 | string status = 1; 60 | } 61 | 62 | message GetUsageForEVSERequest { 63 | int32 evse_id = 1; 64 | } 65 | 66 | message GetUsageForEVSEResponse { 67 | repeated common.v1.Sample samples = 1; 68 | } 69 | 70 | message EVSE { 71 | int32 id = 1; 72 | EVCC evcc = 2; 73 | PowerMeter power_meter = 3; 74 | common.v1.ConnectorStatus status = 4; 75 | optional Session session = 5; 76 | } 77 | 78 | message Session { 79 | string transaction_id = 1; 80 | string tag_id = 2; 81 | google.protobuf.Timestamp started = 3; 82 | string consumption = 4; 83 | } 84 | -------------------------------------------------------------------------------- /proto/evse/v1/power_meter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package evse.v1; 4 | 5 | import "common/v1/hardware.proto"; 6 | 7 | message PowerMeter { 8 | string type = 1; 9 | bool enabled = 2; 10 | optional common.v1.Modbus modbus = 3; 11 | optional common.v1.Spi spi = 4; 12 | } 13 | -------------------------------------------------------------------------------- /proto/logs/v1/log.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package logs.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | service LogService { 8 | rpc GetLogs(google.protobuf.Empty) returns (stream GetLogsResponse) {} 9 | } 10 | 11 | message GetLogsResponse {} 12 | -------------------------------------------------------------------------------- /proto/tags/v1/tags.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tags.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | // Local auth list 9 | service TagService { 10 | rpc GetAuthorizedCards(google.protobuf.Empty) returns (GetAuthorizedCardsResponse) {} 11 | rpc AddAuthorizedCards(AddAuthorizedCardsRequest) returns (AddAuthorizedCardsResponse) {} 12 | rpc RemoveAuthorizedCard(RemoveAuthorizedCardRequest) returns (RemoveAuthorizedCardResponse) {} 13 | } 14 | 15 | message AddAuthorizedCardsRequest { 16 | repeated AuthorizedCard authorized_cards = 1; 17 | } 18 | 19 | message AddAuthorizedCardsResponse { 20 | repeated string status = 1; 21 | } 22 | 23 | message GetAuthorizedCardsResponse { 24 | int32 version = 1; 25 | repeated AuthorizedCard authorized_cards = 2; 26 | } 27 | 28 | message RemoveAuthorizedCardRequest { 29 | string tag_id = 1; 30 | } 31 | 32 | message RemoveAuthorizedCardResponse { 33 | string status = 1; 34 | } 35 | 36 | message AuthorizedCard { 37 | string tag_id = 1; 38 | string status = 2; 39 | optional google.protobuf.Timestamp expiry_date = 3; 40 | } 41 | -------------------------------------------------------------------------------- /proto/users/v1/users.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package users.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | // User management API 8 | service UserService { 9 | rpc AddUser(User) returns (AddUserResponse) {} 10 | rpc GetUsers(google.protobuf.Empty) returns (GetUsersResponse) {} 11 | rpc GetUser(GetUserRequest) returns (User) {} 12 | rpc RemoveUser(RemoveUserRequest) returns (RemoveUserResponse) {} 13 | } 14 | 15 | message User { 16 | string username = 1; 17 | string password = 2; 18 | string role = 3; 19 | } 20 | 21 | message AddUserResponse { 22 | string status = 1; 23 | } 24 | 25 | message GetUsersResponse { 26 | repeated User users = 1; 27 | } 28 | 29 | message GetUserRequest { 30 | string username = 1; 31 | } 32 | 33 | message RemoveUserRequest { 34 | string username = 1; 35 | } 36 | 37 | message RemoveUserResponse { 38 | string status = 1; 39 | } 40 | -------------------------------------------------------------------------------- /scripts/certificates/create-test-certs.sh: -------------------------------------------------------------------------------- 1 | # Inspired by https://github.com/lorenzodonini/ocpp-go/blob/master/example/1.6/create-test-certificates.sh 2 | mkdir -p ../certs/cp 3 | cd certs 4 | # Create CA 5 | echo "Generating CA..." 6 | openssl req -new -x509 -nodes -days 120 -extensions v3_ca -keyout ca.key -out ca.crt -subj "/CN=charge-point" 7 | 8 | # Generate self-signed charge-point certificate 9 | echo "Generating cp private key.." 10 | openssl genrsa -out ../cp/charge-point.key 4096 11 | 12 | echo "Creating sign request.." 13 | openssl req -new -out ../cp/charge-point.csr -key ../cp/charge-point.key -config $1/openssl-cp.conf 14 | 15 | echo "Creating the certificate" 16 | openssl x509 -req -in ../cp/charge-point.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out ../cp/charge-point.crt -days 120 -extensions req_ext -extfile $1/openssl-cp.conf 17 | -------------------------------------------------------------------------------- /scripts/certificates/openssl-cp.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name = req_dn 3 | req_extensions = req_ext 4 | prompt = no 5 | [req_dn] 6 | CN = charge-point 7 | [req_ext] 8 | basicConstraints = CA:FALSE 9 | subjectKeyIdentifier = hash 10 | keyUsage = digitalSignature, keyEncipherment 11 | extendedKeyUsage = clientAuth 12 | subjectAltName = @alt_names 13 | [alt_names] 14 | DNS.1 = charge-point 15 | -------------------------------------------------------------------------------- /scripts/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Update 3 | apt-get update -y 4 | apt-get upgrade -y 5 | 6 | # Install dependencies 7 | apt-get install cmake make autoconf libtool libpcsclite-dev libusb-dev bzip2 -y 8 | 9 | # Download libnfc 10 | wget https://github.com/nfc-tools/libnfc/releases/download/libnfc-1.8.0/libnfc-1.8.0.tar.bz2 11 | tar -xvjf libnfc-1.8.0.tar.bz2 12 | mkdir -p /etc/nfc /etc/nfc/devices.d 13 | cd libnfc-1.8.0 14 | 15 | if [ "$1" = "pn532_i2c" ]; then 16 | # Add configuration for PN532_I2C 17 | touch /etc/nfc/devices.d/pn532_i2c.conf 18 | echo name = "PN532 board via I2C" >>/etc/nfc/devices.d/pn532_i2c.conf 19 | echo connstring = pn532_i2c:/dev/i2c-1 >>/etc/nfc/devices.d/pn532_i2c.conf 20 | echo allow_intrusive_scan = true >>/etc/nfc/devices.d/pn532_i2c.conf 21 | ./configure --with-drivers=pn532_i2c --sysconfdir=/etc --prefix=/usr 22 | 23 | elif [ "$1" = "pn532_spi" ]; then 24 | # Add configuration for PN532_SPI 25 | touch /etc/nfc/devices.d/pn532_spi.conf 26 | echo name = "PN532 board via SPI" >>/etc/nfc/devices.d/pn532_spi.conf 27 | echo connstring = pn532_i2c:/dev/spidev0.0:500000 >>/etc/nfc/devices.d/pn532_spi.conf 28 | echo allow_intrusive_scan = true >>/etc/nfc/devices.d/pn532_spi.conf 29 | ./configure --with-drivers=pn532_spi --sysconfdir=/etc --prefix=/usr 30 | 31 | elif [ "$1" = "pn532_uart" ]; then 32 | # Add configuration for PN532_UART 33 | touch /etc/nfc/devices.d/pn532_uart.conf 34 | echo name = "PN532 board via UART" >>/etc/nfc/devices.d/pn532_uart.conf 35 | echo connstring = pn532_uart:/dev/ttyAMA0 >>/etc/nfc/devices.d/pn532_uart.conf 36 | echo allow_intrusive_scan = true >>/etc/nfc/devices.d/pn532_uart.conf 37 | ./configure --with-drivers=pn532_uart --enable-serial-autoprobe --sysconfdir=/etc --prefix=/usr 38 | fi 39 | 40 | make clean 41 | make install all 42 | 43 | # Install WS281x drivers 44 | cd .. 45 | git clone https://github.com/jgarff/rpi_ws281x 46 | cd rpi_ws281x && mkdir -p build && cd build 47 | 48 | cmake -D BUILD_SHARED=OFF -D BUILD_TEST=ON .. 49 | cmake --build . 50 | make install 51 | cp *.a /usr/local/lib 52 | cp *.h /usr/local/include 53 | 54 | # Optionally, install Go 55 | if [ "$2" -eq 1 ]; then 56 | # installing Go with the help of this script: https://github.com/canha/golang-tools-install-script 57 | wget -q -O - https://git.io/vQhTU | bash 58 | fi 59 | -------------------------------------------------------------------------------- /ui/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{files: ['*.svelte'], processor: 'svelte3/svelte3'}], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript'), 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /ui/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # ChargePi UI 2 | 3 | ## Building 4 | 5 | Install dependencies 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | Build project 12 | 13 | ```bash 14 | npm run build 15 | ``` 16 | 17 | The static output files will be generated in `./build` 18 | 19 | ## Development 20 | 21 | Start the development server 22 | 23 | ```bash 24 | npm run dev 25 | ``` 26 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write ." 13 | }, 14 | "devDependencies": { 15 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 16 | "@sveltejs/adapter-static": "^1.0.0", 17 | "@sveltejs/kit": "^1.0.0", 18 | "@typescript-eslint/eslint-plugin": "^5.45.0", 19 | "@typescript-eslint/parser": "^5.45.0", 20 | "autoprefixer": "^10.4.13", 21 | "daisyui": "^2.46.0", 22 | "eslint": "^8.28.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-svelte3": "^4.0.0", 25 | "postcss": "^8.4.20", 26 | "postcss-load-config": "^4.0.1", 27 | "prettier": "^2.8.0", 28 | "prettier-plugin-svelte": "^2.8.1", 29 | "svelte": "^3.54.0", 30 | "svelte-check": "^2.9.2", 31 | "svelte-fa": "^3.0.3", 32 | "tailwindcss": "^3.2.4", 33 | "theme-change": "^2.2.0", 34 | "tslib": "^2.4.1", 35 | "typescript": "^4.9.3", 36 | "vite": "^4.0.0" 37 | }, 38 | "type": "module" 39 | } 40 | -------------------------------------------------------------------------------- /ui/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /ui/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /ui/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | // and what to do when importing types 4 | declare namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | 11 | declare module '@fortawesome/pro-solid-svg-icons/index.es' { 12 | export * from '@fortawesome/pro-solid-svg-icons'; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ChargePi 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/src/lib/modals/DeleteModal.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /ui/src/lib/navigation/NavItem.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /ui/src/lib/theme/ThemeToggle.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/routes/(auth)/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 |

ChargePi

25 |

An open source Charge Point.

26 |
27 |
28 |
32 | 39 | 40 | 47 | 48 | 49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/evse/+page.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |
28 |
29 |
EVSEs
30 |
{EVSEs}
31 |
32 |
33 | 34 |
35 |
Active sessions
36 |
{ActiveSessions}
37 |
38 | 39 |
40 |
Sessions in past 24h
41 |
{Sessions}
42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {#each Evses as evse} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | {/each} 71 | 72 |
EvseIDStatusEVCCPower MeterMax Power
{evse.EvseID}{evse.Status}{evse.EVCC}{evse.PowerMeter}{evse.MaxPower} 68 | 69 |
73 |
74 |
75 | 76 | -------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/hardware/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | Display 8 | Tag reader 9 | Indication 10 |
11 |
-------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/logs/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {#each logs as log} 30 | 31 | 32 | 33 | 34 | 35 | {/each} 36 | 37 |
TimestampSeverityMessage
{log.timestamp.toISOString()}{""}{log.message}
38 |
39 | -------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/users/+page.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | {#each users as user} 63 | 64 | 65 | 66 | 69 | 70 | {/each} 71 | 72 |
UsernameRole 59 |
{user.username}{user.role} 67 | handleDelete(user)} /> 68 |
73 |
74 | 75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/users/components/CreateUserForm.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 32 | 39 | 45 | 46 |
47 | -------------------------------------------------------------------------------- /ui/src/routes/(dashboard)/users/components/DeleteUserButton.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | Delete user 18 | Are you sure you want to delete this user? 19 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /ui/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 | 14 |
15 | 16 | 31 |
32 | -------------------------------------------------------------------------------- /ui/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChargePi/ChargePi-go/31aac5beb2761cf9757675ca2dd97606498d9b60/ui/static/favicon.png -------------------------------------------------------------------------------- /ui/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import {vitePreprocess} from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | adapter: adapter({ 9 | fallback: 'index.html', 10 | }), 11 | prerender: {entries: []}, 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /ui/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/routes/**/*.{svelte,js,ts}'], 3 | plugins: [require('daisyui')], 4 | daisyui: { 5 | themes: [ 6 | { 7 | dark: { 8 | ...require('daisyui/src/colors/themes')['[data-theme=dark]'], 9 | primary: '#e92b2b', 10 | "secondary": "#ef4444", 11 | 12 | "accent": "#f59e0b", 13 | 14 | "neutral": "#172027", 15 | 16 | "base-100": "#374151", 17 | 18 | "info": "#4b5563", 19 | 20 | "success": "#0F7041", 21 | 22 | "warning": "#F6A431", 23 | 24 | "error": "#E43F42", 25 | }, 26 | }, 27 | { 28 | light: { 29 | ...require('daisyui/src/colors/themes')['[data-theme=light]'], 30 | primary: '#e92b2b', 31 | }, 32 | }, 33 | ], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import {sveltekit} from '@sveltejs/kit/vite'; 2 | 3 | /** @type {import('vite').UserConfig} */ 4 | const config = { 5 | plugins: [sveltekit()], 6 | server: { 7 | fs: { 8 | // Allow serving files from one level up to the project root 9 | allow: ['./static'], 10 | }, 11 | }, 12 | }; 13 | 14 | export default config; 15 | --------------------------------------------------------------------------------