├── .flake8
├── .github
├── dependabot.yml
└── workflows
│ ├── cron.yaml
│ ├── pullpush.yaml
│ └── tag.yaml
├── .gitignore
├── .hound.yml
├── .isort.cfg
├── .markdownlint.json
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── custom_components
└── avanza_stock
│ ├── __init__.py
│ ├── const.py
│ ├── manifest.json
│ └── sensor.py
├── hacs.json
├── renovate.json
└── resources.json
/.flake8:
--------------------------------------------------------------------------------
1 | # Configured to be compliant with black
2 | [flake8]
3 | max-line-length = 88
4 | # E501: line too long
5 | # W503: Line break occurred before a binary operator
6 | # E203: Whitespace before ':'
7 | # D202 No blank lines allowed after function docstring
8 | # W504 line break after binary operator
9 | ignore =
10 | E501,
11 | W503,
12 | E203,
13 | D202,
14 | W504
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: weekly
8 | time: "06:00"
9 | open-pull-requests-limit: 10
10 |
--------------------------------------------------------------------------------
/.github/workflows/cron.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Cron actions
3 |
4 | on: # yamllint disable-line rule:truthy
5 | schedule:
6 | - cron: '0 0 * * *'
7 |
8 | jobs:
9 | validate:
10 | runs-on: "ubuntu-latest"
11 | name: Validate
12 | steps:
13 | - uses: "actions/checkout@v4"
14 |
15 | - name: HACS validation
16 | uses: "hacs/action@main"
17 | with:
18 | CATEGORY: "integration"
19 |
20 | - name: Hassfest validation
21 | uses: "home-assistant/actions/hassfest@master"
22 |
--------------------------------------------------------------------------------
/.github/workflows/pullpush.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Push/Pull actions
3 |
4 | on: # yamllint disable-line rule:truthy
5 | push:
6 | branches:
7 | - main
8 |
9 | pull_request:
10 |
11 | jobs:
12 | validate:
13 | runs-on: "ubuntu-latest"
14 | name: Validate
15 | steps:
16 | - uses: "actions/checkout@v4"
17 |
18 | - name: HACS validation
19 | uses: "hacs/action@main"
20 | with:
21 | CATEGORY: "integration"
22 |
23 | - name: Hassfest validation
24 | uses: "home-assistant/actions/hassfest@master"
25 |
26 | style:
27 | runs-on: "ubuntu-latest"
28 | name: Check style formatting
29 | steps:
30 | - uses: actions/checkout@v4.0.0
31 | - uses: actions/setup-python@v5.1.0
32 | with:
33 | python-version: 3.x
34 | - uses: pre-commit/action@v3.0.1
35 |
--------------------------------------------------------------------------------
/.github/workflows/tag.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Release
3 |
4 | on: # yamllint disable-line rule:truthy
5 | push:
6 | tags:
7 | - 'v*'
8 |
9 | jobs:
10 | release:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - uses: "actions/checkout@v4"
14 | with:
15 | fetch-depth: 0
16 |
17 | - id: changelog
18 | run: |
19 | curr_tag=$(git describe --tags --abbrev=0)
20 | prev_tag=$(git describe --tags --abbrev=0 $curr_tag^)
21 | echo "Previous tag: $prev_tag"
22 | echo "Current tag: $curr_tag"
23 | log="$(git log --format='- %h %s' $prev_tag..$curr_tag)"
24 | log="${log//'%'/'%25'}"
25 | log="${log//$'\n'/'%0A'}"
26 | log="${log//$'\r'/'%0D'}"
27 | echo "::set-output name=body::$log"
28 |
29 | - uses: "actions/create-release@v1"
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | with:
33 | tag_name: ${{ github.ref }}
34 | release_name: Release ${{ github.ref }}
35 | body: |
36 | ## Changes
37 |
38 | ${{ steps.changelog.outputs.body }}
39 | draft: false
40 | prerelease: false
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | ---
2 | fail_on_violations: true
3 |
4 | flake8:
5 | enabled: false
6 | config_file: .flake8
7 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | multi_line_output = 3
3 | include_trailing_comma = True
4 | force_grid_wrap = 0
5 | use_parentheses = True
6 | line_length = 88
7 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "line-length": false
4 | }
5 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v4.5.0
5 | hooks:
6 | - id: trailing-whitespace
7 | - id: end-of-file-fixer
8 | - id: mixed-line-ending
9 | args:
10 | - --fix=lf
11 | - id: check-json
12 | - id: pretty-format-json
13 | args:
14 | - --autofix
15 | - --no-sort-keys
16 |
17 | - repo: https://github.com/psf/black
18 | rev: 23.10.1
19 | hooks:
20 | - id: black
21 | args:
22 | - --safe
23 | - --quiet
24 | files: ^((custom_components)/.+)?[^/]+\.py$
25 |
26 | - repo: https://github.com/pycqa/flake8
27 | rev: 6.1.0
28 | hooks:
29 | - id: flake8
30 | additional_dependencies:
31 | - flake8-docstrings==1.6.0
32 | - pydocstyle==6.1.1
33 | files: ^(custom_components)/.+\.py$
34 |
35 | - repo: https://github.com/PyCQA/isort
36 | rev: 5.12.0
37 | hooks:
38 | - id: isort
39 |
40 | - repo: https://github.com/asottile/pyupgrade
41 | rev: v3.15.0
42 | hooks:
43 | - id: pyupgrade
44 |
45 | - repo: https://github.com/adrienverge/yamllint
46 | rev: v1.32.0
47 | hooks:
48 | - id: yamllint
49 | args:
50 | - --strict
51 |
52 | - repo: https://github.com/igorshubovych/markdownlint-cli
53 | rev: v0.37.0
54 | hooks:
55 | - id: markdownlint
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Claes Hallström
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sensor.avanza_stock
2 |
3 | [![GitHub Release][releases-shield]][releases]
4 | [![GitHub Activity][commits-shield]][commits]
5 | [![License][license-shield]](LICENSE)
6 |
7 | [![hacs][hacsbadge]][hacs]
8 | ![Project Maintenance][maintenance-shield]
9 | [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
10 |
11 | _Custom component to get stock data from [Avanza](https://www.avanza.se) for
12 | [Home Assistant](https://www.home-assistant.io/)._
13 |
14 | ## Note
15 |
16 | The api changed recently, I'm trying to keep as much as possible working but some
17 | attributes may still not work. Please open an issues if you find something. The
18 | plan for the future is to create a new component that should be merged to
19 | homeassitant core.
20 |
21 | ## Installation
22 |
23 | - The easiest way is to install it with [HACS](https://hacs.xyz/). First install
24 | [HACS](https://hacs.xyz/) if you don't have it yet. After installation you can
25 | find this custom component in the HACS store under integrations.
26 |
27 | - Alternatively, you can install it manually. Just copy paste the content of the
28 | `sensor.avanza_stock/custom_components` folder in your `config/custom_components`
29 | directory. As example, you will get the `sensor.py` file in the following path:
30 | `/config/custom_components/avanza_stock/sensor.py`.
31 |
32 | ## Configuration
33 |
34 | key | type | description
35 | :--- | :--- | :---
36 | **platform (Required)** | string | `avanza_stock`
37 | **stock (Required)** | number / list | Stock id or list of stocks, see [here](#stock-configuration) and [here](#finding-stock-or-conversion-currency).
38 | **name (Optional)** | string | Name of the sensor. Default `avanza_stock_{stock}`. Redundant if stock is defined as a list.
39 | **shares (Optional)** | number | Number of shar, can be fractional. Redundant if stock is defined as a list.
40 | **purchase_date (Optional)** | string | Date when stock was purchased. Redundant if stock is defined as a list. Does not get processed further.
41 | **purchase_price (Optional)** | number | Price paid when stock was purchased (per share). Redundant if stock is defined as a list.
42 | **conversion_currency (Optional)** | number | Index id used for currency conversion, see [here](#finding-stock-or-conversion-currency).
43 | **monitored_conditions (Optional)** | list | Attributes to monitor, see [here](#monitored-conditions).
44 | **invert_conversion_currency (Optional)** | boolean | Wether or not to invert the conversion currency, default false.
45 | **currency (Optional)** | string | Overwrite currency given by the api.
46 |
47 | ### Stock configuration
48 |
49 | key | type | description
50 | :--- | :--- | :---
51 | **id (Required)** | number | Stock id, see [here](#finding-stock-or-conversion-currency).
52 | **name (Optional)** | string | Name of the sensor. Default `avanza_stock_{stock}`.
53 | **shares (Optional)** | number | Number of shares, can be fractional.
54 | **purchase_date (Optional)** | string | Date when stock was purchased. Does not get processed further.
55 | **purchase_price (Optional)** | number | Price paid when stock was purchased (per share).
56 | **conversion_currency (Optional)** | number | Index id used for currency conversion, see [here](#finding-stock-or-conversion-currency).
57 | **invert_conversion_currency (Optional)** | boolean | Wether or not to invert the conversion currency, default false.
58 | **currency (Optional)** | string | Overwrite currency given by the api.
59 |
60 | ### Monitored conditions
61 |
62 | If no `monitored_conditions` is defined, change, changePercent and name will be tracked. Full list of available attributes, see [here](custom_components/avanza_stock/const.py#L12) (With the new api change only change and changePercent is currently supported.) Note that the data from the api is not realtime but lagging behind by 15 minutes.
63 |
64 | ### Finding stock or conversion currency
65 |
66 | Got to [Avanza](https://www.avanza.se) and search for the stock you want to track. In the resulting url there is a number, this is the stock id needed for the configuration. Even though it is a Swedish bank it is possible to find stocks from the following countries: Sweden, USA, Denmark, Norway, Finland, Canada, Belgium, France, Italy, Netherlands, Portugal and Germany. To find conversion currencies search for example "USD/SEK" and look for the id in the resulting url. If you can not find your conversion try and search the reverse (NOK/SEK instead of SEK/NOK) and use the `invert_conversion_currency` to get your preffered currency.
67 |
68 | ## Example
69 |
70 | **Configuration with default settings:**
71 |
72 | ```yaml
73 | sensor:
74 | - platform: avanza_stock
75 | stock: 5361
76 | ```
77 |
78 | **Configuration with custom settings:**
79 |
80 | ```yaml
81 | sensor:
82 | - platform: avanza_stock
83 | name: Avanza Bank Holding
84 | stock: 5361
85 | monitored_conditions:
86 | - totalVolumeTraded
87 | - totalValueTraded
88 | ```
89 |
90 | **Configuration with multiple stocks:**
91 |
92 | ```yaml
93 | sensor:
94 | - platform: avanza_stock
95 | stock:
96 | - id: 5361
97 | name: Avanza Bank Holding
98 | - id: 8123
99 | name: Home Assistant
100 | monitored_conditions:
101 | - totalVolumeTraded
102 | - totalValueTraded
103 | ```
104 |
105 | **Configuration with conversion currency:**
106 |
107 | ```yaml
108 | sensor:
109 | - platform: avanza_stock
110 | stock:
111 | - id: 238449
112 | name: TESLA USD
113 | shares: 1
114 | purchase_price: 600
115 | - id: 238449
116 | name: TESLA SEK
117 | shares: 1
118 | conversion_currency: 19000
119 | purchase_price: 7000
120 | - id: 5361
121 | name: Avanza Bank Holding (NOK)
122 | conversion_currency: 53293
123 | invert_conversion_currency: true
124 | ```
125 |
126 | **Configuration with untrackable stock, use id 0 and the purchase_price will be the current state:**
127 |
128 | ```yaml
129 | sensor:
130 | - platform: avanza_stock
131 | stock:
132 | - id: 0
133 | name: MY STOCK
134 | shares: 1
135 | purchase_price: 600
136 | currency: SEK
137 | ```
138 |
139 | ## Usage
140 |
141 | **Automation to send summary at 18:00 using telegram:**
142 |
143 | ```yaml
144 | # Telegram Stock Summary
145 | - alias: 'Telegram Stock Summary'
146 | initial_state: true
147 |
148 | trigger:
149 | - platform: time
150 | at: '18:00:00'
151 |
152 | action:
153 | - service: notify.telegram
154 | data:
155 | message: '
156 | Stock Summary
157 |
158 | {{ states.sensor.avanza_stock_5361.attributes.name }} : {{ states.sensor.avanza_stock_5361.attributes.changePercent }}
159 |
160 | '
161 | ```
162 |
163 | ## Changelog
164 |
165 | - 1.5.3 - Less aggressive rounding
166 | - 1.5.2 - Add minimum home-assistant version
167 | - 1.5.1 - Bump pyavanza (becuase of aiohttp)
168 | - 1.5.0 - Use latest pyavana with latest api changes
169 | - 1.4.0 - Fix more attributes (dure to api changes)
170 | - 1.3.0 - Support manual stock, i.e. not on avanza
171 | - 1.2.0 - Support ETF
172 | - 1.1.2 - Fix dividend
173 | - 1.1.1 - Fix historical changes
174 | - 1.1.0 - Use the new api
175 | - 1.0.12 - Add unique id
176 | - 1.0.11 - Rename device state attributes
177 | - 1.0.10 - Safely remove divident attributes
178 | - 1.0.9 - Add purchase date attribute (string manually set by user)
179 | - 1.0.8 - Correct profit/loss when using conversion currency
180 | - 1.0.7 - Overwrite currency given by the api
181 | - 1.0.6 - Correct unit when using conversion currency
182 | - 1.0.5 - Add option to invert conversion currency
183 | - 1.0.4 - Configure conversion currency and define purchase price
184 | - 1.0.3 - Allow to define multiple stocks
185 | - 1.0.2 - Async update
186 | - 1.0.1 - Allow fractional shares, add more change attributes
187 | - 1.0.0 - Add number of shares as optional configuration
188 | - 0.0.10 - Clean up monitored conditions
189 | - 0.0.9 - Compare payment date with today's date, ignore time
190 | - 0.0.8 - Ignore dividend if amount is zero, add resources.json and manfiest.json
191 | - 0.0.7 - Changed to async setup
192 | - 0.0.6 - Make sure dividend payment date has not passed
193 | - 0.0.5 - Add dividends information
194 | - 0.0.4 - Add companny information (description, marketCapital, sector, totalNumberOfShares)
195 | - 0.0.3 - Add key ratios (directYield, priceEarningsRatio, volatility)
196 | - 0.0.2 - Configure monitored conditions
197 | - 0.0.1 - Initial version
198 |
199 | [buymecoffee]: https://www.buymeacoffee.com/claha
200 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
201 | [commits-shield]: https://img.shields.io/github/commit-activity/y/custom-components/sensor.avanza_stock.svg?style=for-the-badge
202 | [commits]: https://github.com/custom-components/sensor.avanza_stock/commits/master
203 | [hacs]: https://github.com/hacs/integration
204 | [hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
205 | [license-shield]: https://img.shields.io/github/license/custom-components/sensor.avanza_stock.svg?style=for-the-badge
206 | [maintenance-shield]: https://img.shields.io/badge/maintainer-Claes%20Hallström%20%40claha-blue.svg?style=for-the-badge
207 | [releases-shield]: https://img.shields.io/github/release/custom-components/sensor.avanza_stock.svg?style=for-the-badge
208 | [releases]: https://github.com/custom-components/sensor.avanza_stock/releases
209 |
--------------------------------------------------------------------------------
/custom_components/avanza_stock/__init__.py:
--------------------------------------------------------------------------------
1 | """Support for Avanaza Stock sensor."""
2 |
--------------------------------------------------------------------------------
/custom_components/avanza_stock/const.py:
--------------------------------------------------------------------------------
1 | """Constants for avanza_stock."""
2 | __version__ = "1.5.3"
3 |
4 | DEFAULT_NAME = "Avanza Stock"
5 |
6 | CONF_STOCK = "stock"
7 | CONF_SHARES = "shares"
8 | CONF_PURCHASE_DATE = "purchase_date"
9 | CONF_PURCHASE_PRICE = "purchase_price"
10 | CONF_CONVERSION_CURRENCY = "conversion_currency"
11 | CONF_INVERT_CONVERSION_CURRENCY = "invert_conversion_currency"
12 |
13 | MONITORED_CONDITIONS = [
14 | "country",
15 | "currency",
16 | "dividends",
17 | "hasInvestmentFees",
18 | "highestPrice",
19 | "id",
20 | "isin",
21 | "lastPrice",
22 | "lastPriceUpdated",
23 | "loanFactor",
24 | "lowestPrice",
25 | "marketList",
26 | "marketMakerExpected",
27 | "marketTrades",
28 | "morningStarFactSheetUrl",
29 | "name",
30 | "numberOfOwners",
31 | "orderDepthReceivedTime",
32 | "pushPermitted",
33 | "quoteUpdated",
34 | "shortSellable",
35 | "superLoan",
36 | "tradable",
37 | ]
38 |
39 | MONITORED_CONDITIONS_KEYRATIOS = [
40 | "directYield",
41 | "priceEarningsRatio",
42 | "volatility",
43 | ]
44 | MONITORED_CONDITIONS += MONITORED_CONDITIONS_KEYRATIOS
45 |
46 | MONITORED_CONDITIONS_LISTING = [
47 | "tickerSymbol",
48 | "marketPlace",
49 | "flagCode",
50 | ]
51 | MONITORED_CONDITIONS += MONITORED_CONDITIONS_LISTING
52 |
53 | MONITORED_CONDITIONS_COMPANY = [
54 | "description",
55 | "marketCapital",
56 | "sector",
57 | "totalNumberOfShares",
58 | ]
59 | MONITORED_CONDITIONS += MONITORED_CONDITIONS_COMPANY
60 |
61 | MONITORED_CONDITIONS_DIVIDENDS = [
62 | "amount",
63 | "exDate",
64 | "exDateStatus",
65 | "paymentDate",
66 | ]
67 |
68 | MONITORED_CONDITIONS_DEFAULT = [
69 | "change",
70 | "changePercent",
71 | "name",
72 | ]
73 |
74 | MONITORED_CONDITIONS_QUOTE = [
75 | "change",
76 | "changePercent",
77 | "totalValueTraded",
78 | "totalVolumeTraded",
79 | ]
80 | MONITORED_CONDITIONS += MONITORED_CONDITIONS_QUOTE
81 |
82 | MONITORED_CONDITIONS_PRICE = [
83 | "priceAtStartOfYear",
84 | "priceFiveYearsAgo",
85 | "priceOneMonthAgo",
86 | "priceOneWeekAgo",
87 | "priceOneYearAgo",
88 | "priceThreeMonthsAgo",
89 | "priceThreeYearsAgo",
90 | ]
91 | MONITORED_CONDITIONS += MONITORED_CONDITIONS_PRICE
92 |
93 | PRICE_MAPPING = {
94 | "priceAtStartOfYear": "startOfYear",
95 | "priceFiveYearsAgo": "fiveYears",
96 | "priceOneMonthAgo": "oneMonth",
97 | "priceOneWeekAgo": "oneWeek",
98 | "priceOneYearAgo": "oneYear",
99 | "priceThreeMonthsAgo": "rhreeMonths",
100 | "priceThreeYearsAgo": "rhreeYears",
101 | }
102 |
103 | CHANGE_PRICE_MAPPING = [
104 | ("changeOneWeek", "oneWeek"),
105 | ("changeOneMonth", "oneMonth"),
106 | ("changeThreeMonths", "threeMonths"),
107 | ("changeOneYear", "oneYear"),
108 | ("changeThreeYears", "threeYears"),
109 | ("changeFiveYears", "fiveYears"),
110 | ("changeTenYears", "tenYears"),
111 | ("changeCurrentYear", "startOfYear"),
112 | ]
113 |
114 | TOTAL_CHANGE_PRICE_MAPPING = [
115 | ("totalChangeOneWeek", "oneWeek"),
116 | ("totalChangeOneMonth", "oneMonth"),
117 | (
118 | "totalChangeThreeMonths",
119 | "threeMonths",
120 | ),
121 | ("totalChangeOneYear", "oneYear"),
122 | (
123 | "totalChangeThreeYears",
124 | "threeYears",
125 | ),
126 | ("totalChangeFiveYears", "fiveYears"),
127 | ("totalChangeTenYears", "tenYears"),
128 | (
129 | "totalChangeCurrentYear",
130 | "startOfYear",
131 | ),
132 | ]
133 |
134 | CHANGE_PERCENT_PRICE_MAPPING = [
135 | ("changePercentOneWeek", "oneWeek"),
136 | ("changePercentOneMonth", "oneMonth"),
137 | (
138 | "changePercentThreeMonths",
139 | "threeMonths",
140 | ),
141 | ("changePercentOneYear", "oneYear"),
142 | ("changePercentThreeYears", "threeYears"),
143 | ("changePercentFiveYears", "fiveYears"),
144 | ("changePercentTenYears", "tenYears"),
145 | ("changePercentCurrentYear", "startOfYear"),
146 | ]
147 |
148 | CURRENCY_ATTRIBUTE = [
149 | "change",
150 | "highestPrice",
151 | "lastPrice",
152 | "lowestPrice",
153 | "priceAtStartOfYear",
154 | "priceFiveYearsAgo",
155 | "priceOneMonthAgo",
156 | "priceOneWeekAgo",
157 | "priceOneYearAgo",
158 | "priceSixMonthsAgo",
159 | "priceThreeMonthsAgo",
160 | "priceThreeYearsAgo",
161 | "totalValueTraded",
162 | "marketCapital",
163 | "dividend0_amountPerShare",
164 | "dividend1_amountPerShare",
165 | "dividend2_amountPerShare",
166 | "dividend3_amountPerShare",
167 | "dividend4_amountPerShare",
168 | "dividend5_amountPerShare",
169 | "dividend6_amountPerShare",
170 | "dividend7_amountPerShare",
171 | "dividend8_amountPerShare",
172 | "dividend9_amountPerShare",
173 | "changeOneWeek",
174 | "changeOneMonth",
175 | "changeThreeMonths",
176 | "changeSixMonths",
177 | "changeOneYear",
178 | "changeThreeYears",
179 | "changeFiveYears",
180 | "changeCurrentYear",
181 | "totalChangeOneWeek",
182 | "totalChangeOneMonth",
183 | "totalChangeThreeMonths",
184 | "totalChangeSixMonths",
185 | "totalChangeOneYear",
186 | "totalChangeThreeYears",
187 | "totalChangeFiveYears",
188 | "totalChangeCurrentYear",
189 | "totalValue",
190 | "totalChange",
191 | "profitLoss",
192 | "totalProfitLoss",
193 | ]
194 |
--------------------------------------------------------------------------------
/custom_components/avanza_stock/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "avanza_stock",
3 | "name": "Avanza Stock",
4 | "codeowners": [
5 | "@claha"
6 | ],
7 | "dependencies": [],
8 | "documentation": "https://github.com/custom-components/sensor.avanza_stock",
9 | "iot_class": "cloud_polling",
10 | "issue_tracker": "https://github.com/custom-components/sensor.avanza_stock/issues",
11 | "requirements": [
12 | "pyavanza==0.7.1"
13 | ],
14 | "version": "1.5.3"
15 | }
16 |
--------------------------------------------------------------------------------
/custom_components/avanza_stock/sensor.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for getting stock data from avanza.se.
3 |
4 | For more details about this platform, please refer to the documentation at
5 | https://github.com/custom-components/sensor.avanza_stock/blob/master/README.md
6 | """
7 | import logging
8 | from datetime import timedelta
9 |
10 | import homeassistant.helpers.config_validation as cv
11 | import pyavanza
12 | import voluptuous as vol
13 | from homeassistant.components.sensor import (
14 | PLATFORM_SCHEMA,
15 | SensorDeviceClass,
16 | SensorEntity,
17 | SensorStateClass,
18 | )
19 | from homeassistant.const import (
20 | CONF_CURRENCY,
21 | CONF_ID,
22 | CONF_MONITORED_CONDITIONS,
23 | CONF_NAME,
24 | )
25 | from homeassistant.helpers.aiohttp_client import async_create_clientsession
26 |
27 | from custom_components.avanza_stock.const import (
28 | CHANGE_PERCENT_PRICE_MAPPING,
29 | CHANGE_PRICE_MAPPING,
30 | CONF_CONVERSION_CURRENCY,
31 | CONF_INVERT_CONVERSION_CURRENCY,
32 | CONF_PURCHASE_DATE,
33 | CONF_PURCHASE_PRICE,
34 | CONF_SHARES,
35 | CONF_STOCK,
36 | CURRENCY_ATTRIBUTE,
37 | DEFAULT_NAME,
38 | MONITORED_CONDITIONS,
39 | MONITORED_CONDITIONS_COMPANY,
40 | MONITORED_CONDITIONS_DEFAULT,
41 | MONITORED_CONDITIONS_DIVIDENDS,
42 | MONITORED_CONDITIONS_KEYRATIOS,
43 | MONITORED_CONDITIONS_LISTING,
44 | MONITORED_CONDITIONS_PRICE,
45 | MONITORED_CONDITIONS_QUOTE,
46 | PRICE_MAPPING,
47 | TOTAL_CHANGE_PRICE_MAPPING,
48 | )
49 |
50 | _LOGGER = logging.getLogger(__name__)
51 |
52 | SCAN_INTERVAL = timedelta(minutes=60)
53 |
54 | STOCK_SCHEMA = vol.Schema(
55 | {
56 | vol.Required(CONF_ID): cv.positive_int,
57 | vol.Optional(CONF_NAME): cv.string,
58 | vol.Optional(CONF_SHARES): vol.Coerce(float),
59 | vol.Optional(CONF_PURCHASE_DATE): cv.string,
60 | vol.Optional(CONF_PURCHASE_PRICE): vol.Coerce(float),
61 | vol.Optional(CONF_CONVERSION_CURRENCY): cv.positive_int,
62 | vol.Optional(CONF_INVERT_CONVERSION_CURRENCY, default=False): cv.boolean,
63 | vol.Optional(CONF_CURRENCY): cv.string,
64 | }
65 | )
66 |
67 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
68 | {
69 | vol.Required(CONF_STOCK): vol.Any(
70 | cv.positive_int, vol.All(cv.ensure_list, [STOCK_SCHEMA])
71 | ),
72 | vol.Optional(CONF_NAME): cv.string,
73 | vol.Optional(CONF_SHARES): vol.Coerce(float),
74 | vol.Optional(CONF_PURCHASE_DATE): cv.string,
75 | vol.Optional(CONF_PURCHASE_PRICE): vol.Coerce(float),
76 | vol.Optional(CONF_CONVERSION_CURRENCY): cv.positive_int,
77 | vol.Optional(CONF_INVERT_CONVERSION_CURRENCY, default=False): cv.boolean,
78 | vol.Optional(CONF_CURRENCY): cv.string,
79 | vol.Optional(
80 | CONF_MONITORED_CONDITIONS, default=MONITORED_CONDITIONS_DEFAULT
81 | ): vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
82 | }
83 | )
84 |
85 |
86 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
87 | """Set up the Avanza Stock sensor."""
88 | session = async_create_clientsession(hass)
89 | monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
90 | stock = config.get(CONF_STOCK)
91 | entities = []
92 | if isinstance(stock, int):
93 | name = config.get(CONF_NAME)
94 | shares = config.get(CONF_SHARES)
95 | purchase_date = config.get(CONF_PURCHASE_DATE)
96 | purchase_price = config.get(CONF_PURCHASE_PRICE)
97 | conversion_currency = config.get(CONF_CONVERSION_CURRENCY)
98 | invert_conversion_currency = config.get(CONF_INVERT_CONVERSION_CURRENCY)
99 | currency = config.get(CONF_CURRENCY)
100 | if name is None:
101 | name = DEFAULT_NAME + " " + str(stock)
102 | entities.append(
103 | AvanzaStockSensor(
104 | hass,
105 | stock,
106 | name,
107 | shares,
108 | purchase_date,
109 | purchase_price,
110 | conversion_currency,
111 | invert_conversion_currency,
112 | currency,
113 | monitored_conditions,
114 | session,
115 | )
116 | )
117 | _LOGGER.debug("Tracking %s [%d] using Avanza" % (name, stock))
118 | else:
119 | for s in stock:
120 | id = s.get(CONF_ID)
121 | name = s.get(CONF_NAME)
122 | if name is None:
123 | name = DEFAULT_NAME + " " + str(id)
124 | shares = s.get(CONF_SHARES)
125 | purchase_date = s.get(CONF_PURCHASE_DATE)
126 | purchase_price = s.get(CONF_PURCHASE_PRICE)
127 | conversion_currency = s.get(CONF_CONVERSION_CURRENCY)
128 | invert_conversion_currency = s.get(CONF_INVERT_CONVERSION_CURRENCY)
129 | currency = s.get(CONF_CURRENCY)
130 | entities.append(
131 | AvanzaStockSensor(
132 | hass,
133 | id,
134 | name,
135 | shares,
136 | purchase_date,
137 | purchase_price,
138 | conversion_currency,
139 | invert_conversion_currency,
140 | currency,
141 | monitored_conditions,
142 | session,
143 | )
144 | )
145 | _LOGGER.debug("Tracking %s [%d] using Avanza" % (name, id))
146 | async_add_entities(entities, True)
147 |
148 |
149 | class AvanzaStockSensor(SensorEntity):
150 | """Representation of a Avanza Stock sensor."""
151 |
152 | def __init__(
153 | self,
154 | hass,
155 | stock,
156 | name,
157 | shares,
158 | purchase_date,
159 | purchase_price,
160 | conversion_currency,
161 | invert_conversion_currency,
162 | currency,
163 | monitored_conditions,
164 | session,
165 | ):
166 | """Initialize a Avanza Stock sensor."""
167 | self._hass = hass
168 | self._stock = stock
169 | self._name = name
170 | self._shares = shares
171 | self._purchase_date = purchase_date
172 | self._purchase_price = purchase_price
173 | self._conversion_currency = conversion_currency
174 | self._invert_conversion_currency = invert_conversion_currency
175 | self._currency = currency
176 | self._monitored_conditions = monitored_conditions
177 | self._session = session
178 | self._icon = "mdi:cash"
179 | self._state = 0
180 | self._state_attributes = {}
181 | self._unit_of_measurement = ""
182 |
183 | @property
184 | def name(self):
185 | """Return the name of the sensor."""
186 | return self._name
187 |
188 | @property
189 | def icon(self):
190 | """Icon to use in the frontend, if any."""
191 | return self._icon
192 |
193 | @property
194 | def state(self):
195 | """Return the state of the device."""
196 | return self._state
197 |
198 | @property
199 | def extra_state_attributes(self):
200 | """Return the state attributes of the sensor."""
201 | return self._state_attributes
202 |
203 | @property
204 | def unit_of_measurement(self):
205 | """Return the unit of measurement."""
206 | return self._unit_of_measurement
207 |
208 | @property
209 | def unique_id(self):
210 | """Return the unique id."""
211 | return f"{self._stock}_{self._name}_stock"
212 |
213 | @property
214 | def state_class(self):
215 | """Return the state class."""
216 | return SensorStateClass.MEASUREMENT
217 |
218 | @property
219 | def device_class(self):
220 | """Return the device class."""
221 | return SensorDeviceClass.MONETARY
222 |
223 | async def async_update(self):
224 | """Update state and attributes."""
225 | data_conversion_currency = None
226 | if self._stock == 0: # Non trackable, i.e. manual
227 | data = {
228 | "name": self._name.split(" ", 1)[1],
229 | "unit_of_measurement": self._currency,
230 | "quote": {
231 | "last": self._purchase_price,
232 | "change": 0,
233 | "changePercent": 0,
234 | },
235 | "historicalClosingPrices": {
236 | "oneWeek": self._purchase_price,
237 | "oneMonth": self._purchase_price,
238 | "threeMonths": self._purchase_price,
239 | "oneYear": self._purchase_price,
240 | "threeYears": self._purchase_price,
241 | "fiveYears": self._purchase_price,
242 | "tenYears": self._purchase_price,
243 | "startOfYear": self._purchase_price,
244 | },
245 | "listing": {
246 | "currency": self._currency,
247 | },
248 | }
249 | else:
250 | data = await pyavanza.get_stock_async(self._session, self._stock)
251 | if data["type"] == pyavanza.InstrumentType.ExchangeTradedFund:
252 | data = await pyavanza.get_etf_async(self._session, self._stock)
253 | if self._conversion_currency:
254 | data_conversion_currency = await pyavanza.get_stock_async(
255 | self._session, self._conversion_currency
256 | )
257 | if data:
258 | self._update_state(data)
259 | self._update_unit_of_measurement(data)
260 | self._update_state_attributes(data)
261 | if data_conversion_currency:
262 | self._update_conversion_rate(data_conversion_currency)
263 | if self._currency:
264 | self._unit_of_measurement = self._currency
265 |
266 | def _update_state(self, data):
267 | self._state = data["quote"]["last"]
268 |
269 | def _update_unit_of_measurement(self, data):
270 | self._unit_of_measurement = data["listing"]["currency"]
271 |
272 | def _update_state_attributes(self, data):
273 | for condition in self._monitored_conditions:
274 | if condition in MONITORED_CONDITIONS_KEYRATIOS:
275 | self._update_key_ratios(data, condition)
276 | elif condition in MONITORED_CONDITIONS_COMPANY:
277 | self._update_company(data, condition)
278 | elif condition in MONITORED_CONDITIONS_QUOTE:
279 | self._update_quote(data, condition)
280 | elif condition in MONITORED_CONDITIONS_LISTING:
281 | self._update_listing(data, condition)
282 | elif condition in MONITORED_CONDITIONS_PRICE:
283 | self._update_price(data, condition)
284 | elif condition == "dividends":
285 | self._update_dividends(data)
286 | elif condition == "id":
287 | self._state_attributes[condition] = data.get("orderbookId", None)
288 | else:
289 | self._state_attributes[condition] = data.get(condition, None)
290 |
291 | if condition == "change":
292 | for change, price in CHANGE_PRICE_MAPPING:
293 | if price in data["historicalClosingPrices"]:
294 | self._state_attributes[change] = round(
295 | data["quote"]["last"]
296 | - data["historicalClosingPrices"][price],
297 | 5,
298 | )
299 | else:
300 | self._state_attributes[change] = "unknown"
301 |
302 | if self._shares is not None:
303 | for change, price in TOTAL_CHANGE_PRICE_MAPPING:
304 | if price in data["historicalClosingPrices"]:
305 | self._state_attributes[change] = round(
306 | self._shares
307 | * (
308 | data["quote"]["last"]
309 | - data["historicalClosingPrices"][price]
310 | ),
311 | 5,
312 | )
313 | else:
314 | self._state_attributes[change] = "unknown"
315 |
316 | if condition == "changePercent":
317 | for change, price in CHANGE_PERCENT_PRICE_MAPPING:
318 | if price in data["historicalClosingPrices"]:
319 | self._state_attributes[change] = round(
320 | 100
321 | * (
322 | data["quote"]["last"]
323 | - data["historicalClosingPrices"][price]
324 | )
325 | / data["historicalClosingPrices"][price],
326 | 3,
327 | )
328 | else:
329 | self._state_attributes[change] = "unknown"
330 |
331 | if self._shares is not None:
332 | self._state_attributes["shares"] = self._shares
333 | self._state_attributes["totalValue"] = round(
334 | self._shares * data["quote"]["last"], 5
335 | )
336 | self._state_attributes["totalChange"] = round(
337 | self._shares * data["quote"]["change"], 5
338 | )
339 |
340 | self._update_profit_loss(data["quote"]["last"])
341 |
342 | def _update_key_ratios(self, data, attr):
343 | key_ratios = data.get("keyRatios", {})
344 | self._state_attributes[attr] = key_ratios.get(attr, None)
345 |
346 | def _update_company(self, data, attr):
347 | company = data.get("company", {})
348 | self._state_attributes[attr] = company.get(attr, None)
349 |
350 | def _update_quote(self, data, attr):
351 | quote = data.get("quote", {})
352 | self._state_attributes[attr] = quote.get(attr, None)
353 |
354 | def _update_listing(self, data, attr):
355 | listing = data.get("listing", {})
356 | if attr == "marketPlace":
357 | self._state_attributes[attr] = listing.get("marketPlaceName", None)
358 | elif attr == "flagCode":
359 | self._state_attributes[attr] = listing.get("countryCode", None)
360 | else:
361 | self._state_attributes[attr] = listing.get(attr, None)
362 |
363 | def _update_price(self, data, attr):
364 | prices = data.get("historicalClosingPrices", {})
365 | self._state_attributes[attr] = prices.get(PRICE_MAPPING[attr], None)
366 |
367 | def _update_profit_loss(self, price):
368 | if self._purchase_date is not None:
369 | self._state_attributes["purchaseDate"] = self._purchase_date
370 | if self._purchase_price is not None:
371 | self._state_attributes["purchasePrice"] = self._purchase_price
372 | self._state_attributes["profitLoss"] = round(
373 | price - self._purchase_price, 5
374 | )
375 | self._state_attributes["profitLossPercentage"] = round(
376 | 100 * (price - self._purchase_price) / self._purchase_price, 3
377 | )
378 |
379 | if self._shares is not None:
380 | self._state_attributes["totalProfitLoss"] = round(
381 | self._shares * (price - self._purchase_price), 5
382 | )
383 |
384 | def _update_conversion_rate(self, data):
385 | rate = data["quote"]["last"]
386 | if self._invert_conversion_currency:
387 | rate = 1.0 / rate
388 | self._state = round(self._state * rate, 5)
389 | if self._invert_conversion_currency:
390 | self._unit_of_measurement = data["name"].split("/")[0]
391 | else:
392 | self._unit_of_measurement = data["name"].split("/")[1]
393 | for attribute in self._state_attributes:
394 | if (
395 | attribute in CURRENCY_ATTRIBUTE
396 | and self._state_attributes[attribute] is not None
397 | and self._state_attributes[attribute] != "unknown"
398 | ):
399 | self._state_attributes[attribute] = round(
400 | self._state_attributes[attribute] * rate, 5
401 | )
402 | self._update_profit_loss(self._state)
403 |
404 | def _update_dividends(self, data):
405 | if "keyIndicators" not in data:
406 | return
407 | if "dividend" not in data["keyIndicators"]:
408 | return
409 |
410 | dividend = data["keyIndicators"]["dividend"]
411 | for dividend_condition in MONITORED_CONDITIONS_DIVIDENDS:
412 | if dividend_condition not in dividend:
413 | continue
414 | attribute = "dividend_{}".format(dividend_condition)
415 | self._state_attributes[attribute] = dividend[dividend_condition]
416 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Avanza Stock",
3 | "render_readme": true,
4 | "homeassistant": "2024.1.6"
5 | }
6 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | ":enablePreCommit"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/resources.json:
--------------------------------------------------------------------------------
1 | [
2 | "https://raw.githubusercontent.com/custom-components/sensor.avanza_stock/master/custom_components/avanza_stock/__init__.py",
3 | "https://raw.githubusercontent.com/custom-components/sensor.avanza_stock/master/custom_components/avanza_stock/manifest.json"
4 | ]
5 |
--------------------------------------------------------------------------------